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

import sys
import pickle
import random
import tempfile
import functions
from model import Model
from minimclass import Minim

try:
    import pygtk
    pygtk.require("2.0")
    import gobject
except:
    print("pygtk Not Availible")
    sys.exit(1)
try:
    import gtk
except:
    print("GTK Not Availible")
    sys.exit(1)

class ModelInteface(object):
    def close_window(self, widget, data=None):
        if self.trial_file_name:
            self.save_trial(self.trial_file_name)        
        self.window.destroy()
        if __name__ == "__main__":
            sys.exit(0)

    def delete_event(self, widget, event):
        if self.trial_file_name:
            self.save_trial(self.trial_file_name)        
        self.window.destroy()
        if __name__ == "__main__":
            sys.exit(0)

    def group_cell_edited(self, cell, row, new_text, col):
        if col == 0:
            # names must not be repeated
            for r in range(len(self.group_liststore)):
                if r != row and new_text == self.group_liststore[r][0]:
                    self.statusbar.pop(self.sb_context_id)
                    self.statusbar.push(self.sb_context_id, "Error: Group name already in use! Choose another name!")
                    return False
        if len(new_text) == 0:
            self.statusbar.pop(self.sb_context_id)
            self.statusbar.push(self.sb_context_id, "Error: Group name can not be empty!")
            return False
        if col == 1:
            if not new_text.isdigit():
                self.statusbar.pop(self.sb_context_id)
                self.statusbar.push(self.sb_context_id, "Error: Allocation ratio must be an integer!")
                return False
            new_text = int(new_text)
            if not new_text:
                self.statusbar.pop(self.sb_context_id)
                self.statusbar.push(self.sb_context_id, "Error: Allocation ratio must be zero!")
                return False
        self.group_liststore[row][col] = new_text
        self.update_allocations()

    def group_add_button(self, widget, data=None):
        self.group_liststore.append(('Group {0}'.format(len(self.group_liststore)+1), 1))
        self.update_allocations()

    def group_delete_button(self, widget, data=None):
        if self.group_treeview.get_cursor()[0]:
            row = self.group_treeview.get_cursor()[0][0]
            self.group_liststore.remove(self.group_liststore.get_iter(row))
            for idx, record in enumerate(self.allocations):
                if record['allocation'] == row:
                    self.ui_pool.append(self.allocations[idx]['UI'])
                    self.allocations[idx] = False
                elif record['allocation'] > row:
                    record['allocation'] -= 1
            self.allocations = filter(None, self.allocations)
            self.update_allocations()
        else:
            self.statusbar.pop(self.sb_context_id)
            self.statusbar.push(self.sb_context_id, "Error: Please select a group first!")
            return False

    def variable_cell_edited(self, cell, row, new_text, col):
        if col == 0:
            for r in range(len(self.variable_liststore)):
                if r != row and new_text == self.variable_liststore[r][0]:
                    self.statusbar.pop(self.sb_context_id)
                    self.statusbar.push(self.sb_context_id, "Error: This variable name already in use! Choose another name!")
                    return False
        if len(new_text) == 0:
            self.statusbar.pop(self.sb_context_id)
            self.statusbar.push(self.sb_context_id, "Error: Variable names can not be empty!")
            return False
        if col == 1:
            new_text = float(new_text)
            if not new_text:
                self.statusbar.pop(self.sb_context_id)
                self.statusbar.push(self.sb_context_id, "Error: Variable weight can not be zero!")
                return False
        if col == 2:
            # check for deleting a level
            old_levels = self.variable_liststore[row][col].split(',')
            new_levels = new_text.split(',')
            if len(new_levels) < 2:
                self.statusbar.pop(self.sb_context_id)
                self.statusbar.push(self.sb_context_id, "Error: At least two variable levels should be entered! Separate levels with commas")
                return False
            if len(old_levels) > len(new_levels):
                # which levels has been deleted?
                del_levels = list(set(old_levels) - set(new_levels))
                # convert items to indices
                del_levels = [old_levels.index(level) for level in del_levels]
                del_levels.sort()
                for idx in range(len(self.allocations)):
                    for i, level in enumerate(self.allocations[idx]['levels']):
                        if level in del_levels:
                            self.ui_pool.append(self.allocations[idx]['UI'])
                            self.allocations[idx] = False
                # deleting false items
                self.allocations = filter(None, self.allocations)
                for idx in range(len(self.allocations)):
                    for i, level in enumerate(self.allocations[idx]['levels']):
                        d = 0
                        for del_level in del_levels:
                            # for each del_level less than level the level index number should decrement one unit
                            if level > del_level:
                                d += 1
                        self.allocations[idx]['levels'][i] -= d
        self.variable_liststore[row][col] = new_text
        self.update_allocations()

    def variable_add_button(self, widget, data=None):
        self.variable_liststore.append(('Variable {0}'.format(len(self.variable_liststore)+1), 1.0, 'Level 0, Level 1'))
        self.update_allocations()

    def variable_delete_button(self, widget, data=None):
        if self.variable_treeview.get_cursor()[0]:
            row = self.variable_treeview.get_cursor()[0][0]
            self.variable_liststore.remove(self.variable_liststore.get_iter(row))
            for idx in range(len(self.allocations)):
                self.allocations[idx]['levels'].pop(row)
            self.update_allocations()

    def update_allocations(self):
        n = len(self.allocations_treeview.get_columns())
        while n:
            n = self.allocations_treeview.remove_column(self.allocations_treeview.get_column(n-1))
        widgets = self.combo_vbox.get_children()
        for widget in widgets:
            widget.destroy()
        table = gtk.Table(3, len(self.variable_liststore)+1)
        min_group_old_index = self.min_group_combo.get_active()
        if min_group_old_index < 0:
            min_group_old_index = 0
        if (min_group_old_index + 1) > len(self.group_liststore):
            min_group_old_index = 0
        self.min_group_combo.get_model().clear()
        for group in self.group_liststore:
            self.min_group_combo.append_text(group[0])
        if len(self.group_liststore):
            self.min_group_combo.set_active(min_group_old_index)
        self.variable_combos = []
        n = 0
        for variable in self.variable_liststore:
            lbl = gtk.Label(variable[0])
            table.attach(lbl, 0, 1, n, n+1)
            cbo = gtk.combo_box_new_text()
            for level in map(str.strip, variable[2].split(',')):
                cbo.append_text(level)
            self.variable_combos.append(cbo)
            table.attach(cbo, 1, 2, n, n+1)
            n += 1
        self.combo_vbox.pack_start(table, False, False)
        types = ((str,) * (len(self.variable_liststore) + 2))
        liststore = gtk.ListStore(*types)

        tvcolumn = gtk.TreeViewColumn('UI')
        self.allocations_treeview.append_column(tvcolumn)
        cell = gtk.CellRendererText()
        tvcolumn.pack_start(cell, True)        
        tvcolumn.add_attribute(cell, 'text', 0)
        tvcolumn.set_resizable(True)
        tvcolumn.set_expand(True)

        tvcolumn = gtk.TreeViewColumn('Group')
        self.allocations_treeview.append_column(tvcolumn)
        cell = gtk.CellRendererText()
        tvcolumn.pack_start(cell, True)        
        tvcolumn.add_attribute(cell, 'text', 1)
        tvcolumn.set_resizable(True)
        tvcolumn.set_expand(True)

        n = 1
        for variable in self.variable_liststore:
            n += 1
            tvcolumn = gtk.TreeViewColumn(variable[0])
            self.allocations_treeview.append_column(tvcolumn)
            cell = gtk.CellRendererText()
            tvcolumn.pack_start(cell, True)        
            tvcolumn.add_attribute(cell, 'text', n)
            tvcolumn.set_resizable(True)
            tvcolumn.set_expand(True)
        self.allocations_treeview.set_model(liststore)
        variable_models = [cbo.get_model() for cbo in self.variable_combos]
        for row, record in enumerate(self.allocations):
            liststore.append()
            liststore[row][0] = record['UI']
            liststore[row][1] = self.group_liststore[record['allocation']][0]
            for col, variable_model in enumerate(variable_models):
                liststore[row][col+2] = variable_model[record['levels'][col]][0]
        self.window.show_all()

    def get_minimize_case(self, new_case):
        model = Model()
        model.groups = range(len(self.group_liststore))
        model.variables = [range(len(self.variable_liststore[row][2].split(','))) for row in range(len(self.variable_liststore))]
        model.variables_weight = [self.variable_liststore[row][1] for row in range(len(self.variable_liststore))]
        model.allocation_ratio = [self.group_liststore[row][1] for row in range(len(self.group_liststore))]
        model.allocations = self.allocations
        model.prob_method = self.get_prob_method()
        model.distance_measure = self.get_distance_measure()
        model.high_prob = self.high_prob_spin.get_value()
        model.min_group = self.min_group_combo.get_active()
        m = Minim(model)
        m.build_probs(model.high_prob, model.min_group)
        m.build_freq_table()
        if self.initial_freq_table:
            self.add_to_initial(m.freq_table)
        return m.enroll(new_case, m.freq_table)

    def add_to_initial(self, freq_table):
        for row, group in enumerate(freq_table):
            for v, variable in enumerate(group):
                for l, level in enumerate(variable):
                    freq_table[row][v][l] += self.initial_freq_table[row][v][l]

    def allocations_undo_button(self, widget, data=None):
        model = self.allocations_treeview.get_model()
        if model == None or len(model) == 0:
            self.statusbar.pop(self.sb_context_id)
            self.statusbar.push(self.sb_context_id, "Error: No subject allocated!")
            if self.trial_file_name:
                self.want_trial_unlock()
            return False
        msg = "Deallocation may introduce errors in randomization!\nAre you sure you want to undo the last allocation?"
        dialog = gtk.MessageDialog(self.window, flags = gtk.DIALOG_MODAL, type = gtk.MESSAGE_QUESTION, buttons=gtk.BUTTONS_YES_NO, message_format = msg)
        dialog.set_title("Undo Allocation!")
        if dialog.run() == gtk.RESPONSE_YES:
            row = len(model)-1
            self.ui_pool.append(model[row][0])
            path = (row,)
            iter = model.get_iter(path)
            model.remove(iter)
            self.allocations.pop()
            self.save_trial(self.trial_file_name)
        dialog.destroy()

    def allocations_add_button(self, widget, data=None):
        model = self.allocations_treeview.get_model()
        if not model:
            self.statusbar.pop(self.sb_context_id)
            self.statusbar.push(self.sb_context_id, "Error: Groups and prognostic factors must be defined first!")
            return False
        for cbo in self.variable_combos:
            if cbo.get_active() == -1:
                self.statusbar.pop(self.sb_context_id)
                self.statusbar.push(self.sb_context_id, 'Error:  Some factor levels have not been selected for this case!')
                return False
        if not self.ui_pool:
            self.statusbar.pop(self.sb_context_id)
            self.statusbar.push(self.sb_context_id, 'Error: Either this trial is finished or has not been saved!')
            return False
        model.append()
        ui = self.ui_pool.pop()
        new_case = {'levels': [], 'allocation': -1, 'UI': ui}
        row = len(model)-1
        model[row][0] = ui
        for col, cbo in enumerate(self.variable_combos):
            model[row][col+2] = cbo.get_active_text()
            new_case['levels'].append(cbo.get_active())
        m_group = self.get_minimize_case(new_case)
        new_case['allocation'] = m_group
        model[row][1] = self.group_liststore[m_group][0]
        self.allocations.append(new_case)
        self.save_trial(self.trial_file_name)
        self.report_allocation(model)
        if len(self.ui_pool) == 0:
           self.end_trial()

    def report_allocation(self, model):
        column = self.allocations_treeview.get_columns()
        row = len(model)-1
        ret = []
        for col, column in enumerate(column):
            ret.append(column.get_title() + ': ' + model[row][col])

        msg = "New case allocated\n" + '\n'.join(ret)
        dialog = gtk.MessageDialog(self.window, flags = gtk.DIALOG_MODAL, type = gtk.MESSAGE_INFO, buttons=gtk.BUTTONS_OK, message_format = msg)
        dialog.set_title("New Allocation!")
        dialog.run()
        dialog.destroy()

    def end_trial(self):
        msg = "{0} cases have been minimized!\nDo you like to extend the sample size".format(len(self.allocations))
        dialog = gtk.MessageDialog(self.window, flags = gtk.DIALOG_MODAL, type = gtk.MESSAGE_QUESTION, buttons=gtk.BUTTONS_YES_NO, message_format = msg)
        dialog.set_title("Trial finished!")
        if dialog.run() == gtk.RESPONSE_YES:
            extra_sample = ""
            while not extra_sample.isdigit():
                extra_sample = functions.simple_dialog("Extra Sample", self.window)
            ss = len(self.allocations)+int(extra_sample)
            self.build_ui_pool(range(len(self.allocations), ss))
            model = self.allocations_treeview.get_model()
            for row in range(len(model)):
                model[row][0] = model[row][0].zfill(len(str(ss)))
        else:
            self.ui_pool = None
        dialog.destroy()

    def refresh_freq_table(self):
        model = Model()
        model.groups = range(len(self.group_liststore))
        model.variables = [range(len(self.variable_liststore[row][2].split(','))) for row in range(len(self.variable_liststore))]
        model.variables_weight = [self.variable_liststore[row][1] for row in range(len(self.variable_liststore))]
        model.allocation_ratio = [self.group_liststore[row][1] for row in range(len(self.group_liststore))]
        model.allocations = self.allocations
        m = Minim(model)
        m.build_freq_table()
        if self.initial_freq_table:
            self.add_to_initial(m.freq_table)
        if not len(m.freq_table):
            self.statusbar.pop(self.sb_context_id)
            self.statusbar.push(self.sb_context_id, "Error: No allocation!")
            return False
        n = len(self.freq_table_treeview.get_columns())
        while n:
            n = self.freq_table_treeview.remove_column(self.freq_table_treeview.get_column(n-1))
        self.initial_table_vbox.hide()
        # following for test only
        # m.freq_table = [[[18, 25], [6, 3, 3, 4, 3, 6, 4, 5, 5, 4]], [[39, 45], [12, 8, 7, 7, 7, 9, 6, 12, 11, 5]], [[55, 68], [17, 12, 10, 14, 10, 13, 8, 16, 15, 8]]]
        cols = 0
        column_names = []
        for variable in self.variable_liststore:
            column_names.extend([level for level in map(str.strip, variable[2].split(','))])
        column_names = ['Group'] + column_names + ['Total']
        for var in m.freq_table[0]:
            cols += len(var)
        col_types = [str] + [int] * (cols + 1)
        liststore = gtk.ListStore(*col_types)        
        for n in range(len(column_names)):
            tvcolumn = gtk.TreeViewColumn(column_names[n])
            self.freq_table_treeview.append_column(tvcolumn)
            cell = gtk.CellRendererText()
            #cell.set_property('editable', True)
            #cell.connect("edited", edit_handler, n)
            tvcolumn.pack_start(cell, True)
            tvcolumn.add_attribute(cell, 'text', n)
            tvcolumn.set_resizable(True)
            tvcolumn.set_expand(True)
        self.freq_table_treeview.set_model(liststore)
        for row, group in enumerate(m.freq_table):
            liststore.append()
            liststore[row][0] = self.group_liststore[row][0]
            col = 1
            for variable in group:
                for level in variable:
                    liststore[row][col] = level
                    col += 1
            liststore[row][col] = sum(variable)
        liststore.append()
        liststore[len(m.freq_table)][0] = "Total"
        total = 0
        for c in range(1, col):
            col_total = sum([liststore[row][c] for row in range(len(m.freq_table))])
            total += col_total
            liststore[len(m.freq_table)][c] = col_total
        liststore[len(m.freq_table)][c+1] = total / len(self.variable_liststore)
        if self.freq_table_enable_edit_button.get_label() == "Save as Initial Table":
            self.freq_table_enable_edit()
        self.window.show_all()

    def notebook_switch_page(self, notebook, page, page_num, data=None):
        self.statusbar.pop(self.sb_context_id)
        self.statusbar.push(self.sb_context_id, self.notbook_tabs[page_num])
        if page_num < 4: return
        self.refresh_freq_table()
        self.balance_table_refresh()

    def license_button_clicked(self, widget, data=None):
        self.display_about_dialog()

    def display_about_dialog(self):
        dialog = gtk.AboutDialog()
        dialog.set_name("MinimPy Program")
        dialog.set_version("0.1")
        dialog.set_copyright("Copyright (c) 2010-2011 Mahmoud Saghaei")
        dialog.set_license("Distributed under the GNU GPL v3.\nFor full terms refer to http://www.gnu.org/copyleft/gpl.html.")
        dialog.set_website("http://minimpy.sourceforge.net")
        dialog.set_authors(["Mahmoud Saghaei"])
        dialog.set_logo(self.pixbuf)
        dialog.run()
        dialog.destroy()
        return False
        
        dialog = gtk.MessageDialog(self.window, flags = gtk.DIALOG_MODAL, type = 
gtk.MESSAGE_INFO, buttons=gtk.BUTTONS_OK, message_format = "XKnight game\nCopyright (c) 2008-2009 Mahmoud Saghaei\nDistributed under the GNU GPL v3.\nFor full terms refer to http://www.gnu.org/copyleft/gpl.html.")
        dialog.set_title("About XKnight Game!")
        dialog.run()
        dialog.destroy()

    def trial_properties_cell_edited(self, cell, row, new_text, col):
        self.trial_properties_liststore[row][col] = new_text

    def trial_properties_add_button(self, widget, data=None):
        self.trial_properties_liststore.append(('Property Name', 'Property Value'))

    def trial_properties_delete_button(self, widget, data=None):
        if self.trial_properties_treeview.get_cursor()[0]:
            row = self.trial_properties_treeview.get_cursor()[0][0]
            self.trial_properties_liststore.remove(self.trial_properties_liststore.get_iter(row))

    def __init__(self):
        self.trial_file_name = None
        self.notbook_tabs = []
        self.ui_pool = None
        self.initial_freq_table = False
        self.allocations = []
        self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
        self.window.set_title("Minimization Program")
        self.window.connect("delete_event", self.delete_event)
        self.window.set_border_width(2)
        self.window.set_size_request(750, 400)
        self.notebook = gtk.Notebook()
        self.notebook.set_tab_pos(gtk.POS_LEFT)
        self.notebook.popup_enable()
        self.notebook.connect("switch-page", self.notebook_switch_page)
        img = gtk.Image()
        img.set_from_file('logo.png')
        img.show()
        self.pixbuf = img.get_pixbuf()        

        win_vbox = gtk.VBox(False)
        win_vbox.pack_start(self.notebook, True, True)

        self.statusbar = gtk.Statusbar()
        win_vbox.pack_start(self.statusbar, False, False, 0)
        self.sb_context_id = self.statusbar.get_context_id("Statusbar")

        vbx = gtk.VBox(False)
        hbx = gtk.HBox(False)
        label = gtk.Label("Trial Title")
        hbx.pack_start(label, False, False)
        self.trial_title_entry = gtk.Entry()
        hbx.pack_start(self.trial_title_entry, True, True)

        label = gtk.Label("Sample Size")
        hbx.pack_start(label, False, False)
        adj = gtk.Adjustment(value=30, lower=5, upper=1000000, step_incr=1, page_incr=10)
        self.trial_sample_size_spin = gtk.SpinButton(adj)
        hbx.pack_start(self.trial_sample_size_spin, False, False)
        vbx.pack_start(hbx, False, False)

        hbx = gtk.HBox(False)
        label = gtk.Label("Description")
        hbx.pack_start(label, False, False)
        vbx.pack_start(hbx, False, False)
        sep = gtk.HSeparator()
        vbx.pack_start(sep, False, False)
        hbx = gtk.HBox(False)
        sep = gtk.VSeparator()
        hbx.pack_start(sep, False, False)
        self.trial_description_text = gtk.TextView()
        hbx.pack_start(self.trial_description_text, True, True)
        sep = gtk.VSeparator()
        hbx.pack_start(sep, False, False)
        vbx.pack_start(hbx, True, True)
        sep = gtk.HSeparator()
        vbx.pack_start(sep, False, False)

        hbox = gtk.HBox(False)
        vbx.pack_start(hbox, True, True)
        label = gtk.Label("Probability Method")
        vbox = gtk.VBox(False)
        vbox.pack_start(label, False, False)
        radio = gtk.RadioButton(None, label="Biased Coin Minimization")
        vbox.pack_start(radio, False, False)
        radio = gtk.RadioButton(radio, label="Naive Minimization")
        vbox.pack_start(radio, False, False)
        # this method returns the radios in revese order
        self.prob_method_radios = radio.get_group()
        # so we need to invert the returned list
        self.prob_method_radios.reverse()

        hb = gtk.HBox(False)
        lbl = gtk.Label("Min Group")
        hb.pack_start(lbl, False, False)
        self.min_group_combo = gtk.combo_box_new_text()
        hb.pack_start(self.min_group_combo, False, False)
        vbox.pack_start(hb, False, False)

        hb = gtk.HBox(False)
        lbl = gtk.Label("High Probability")
        hb.pack_start(lbl, False, False)
        adj = gtk.Adjustment(value=0.7, lower=0.1, upper=1.0, step_incr=0.01, page_incr=0.1)
        self.high_prob_spin = gtk.SpinButton(adj, digits=2)
        hb.pack_start(self.high_prob_spin, False, False)
        vbox.pack_start(hb, False, False)
        hbox.pack_start(vbox, False, False)
        sep = gtk.VSeparator()
        hbox.pack_start(sep, False, False)

        label = gtk.Label("Distance Measure")
        vbox = gtk.VBox(False)
        vbox.pack_start(label, False, False)
        radio = gtk.RadioButton(None, label="Marginal Balance")
        vbox.pack_start(radio, False, False)
        radio = gtk.RadioButton(radio, label="Range")
        vbox.pack_start(radio, False, False)
        radio = gtk.RadioButton(radio, label="Standard Deviation")
        vbox.pack_start(radio, False, False)
        radio = gtk.RadioButton(radio, label="Variance")
        vbox.pack_start(radio, False, False)
        hbox.pack_start(vbox, False, False)
        self.distance_measure_radios = radio.get_group()
        self.distance_measure_radios.reverse()
        sep = gtk.VSeparator()
        hbox.pack_start(sep, False, False)

        vbox = gtk.VBox(False)
        lbl = gtk.Label("Trial Properties")
        vbox.pack_start(lbl, False, False)
        hb = gtk.HBox(False)
        self.trial_properties_liststore, self.trial_properties_treeview = functions.make_treeview(
        (str, str), ['Name', 'Value'],
        editable=True, edit_handler=self.trial_properties_cell_edited)
        hb.pack_start(self.trial_properties_treeview, True, True)
        vbuttonbox = gtk.VButtonBox()
        vbuttonbox.set_layout(gtk.BUTTONBOX_START)
        button = gtk.Button(None, gtk.STOCK_ADD)
        button.connect("clicked", self.trial_properties_add_button)
        vbuttonbox.pack_start(button, False, False)
        button = gtk.Button(None, gtk.STOCK_DELETE)
        button.connect("clicked", self.trial_properties_delete_button)
        vbuttonbox.pack_start(button, False, False)
        hb.pack_start(vbuttonbox, False, False)
        vbox.pack_start(hb, False, False)

        hbox.pack_start(vbox, True, True)
        sep = gtk.VSeparator()
        hbox.pack_start(sep, False, False)

        sep = gtk.HSeparator()
        vbx.pack_start(sep, False, False)

        hbuttonbox = gtk.HButtonBox()
        hbuttonbox.set_layout(gtk.BUTTONBOX_START)

        self.trial_load_button = gtk.Button(None, gtk.STOCK_OPEN)
        self.trial_load_button.connect("clicked", self.load_trial_clicked)
        hbuttonbox.pack_start(self.trial_load_button, False, False)

        self.trial_save_button = gtk.Button(None, gtk.STOCK_SAVE)
        self.trial_save_button.connect("clicked", self.save_trial_clicked)
        hbuttonbox.pack_start(self.trial_save_button, False, False)

        vbx.pack_start(hbuttonbox, False, False)

        lbl = gtk.Label("Settings")
        self.notbook_tabs.append("Settings: Define and save a new trial or load a previous trial")
        self.notebook.append_page(vbx, lbl)

        hbox = gtk.HBox(False)
        self.group_liststore, self.group_treeview = functions.make_treeview(
        (str, int), ['Group Name', 'Allocation Ratio'],
        editable=True, edit_handler=self.group_cell_edited)
        hbox.pack_start(self.group_treeview, True, True)
        vbuttonbox = gtk.VButtonBox()
        vbuttonbox.set_layout(gtk.BUTTONBOX_START)
        button = gtk.Button(None, gtk.STOCK_ADD)
        button.connect("clicked", self.group_add_button)
        vbuttonbox.pack_start(button, False, False)
        button = gtk.Button(None, gtk.STOCK_DELETE)
        button.connect("clicked", self.group_delete_button)
        vbuttonbox.pack_start(button, False, False)
        hbox.pack_start(vbuttonbox, False, False)
        lbl = gtk.Label('Groups')
        self.notbook_tabs.append("Groups: Define trial groups and their allocation ratios")
        self.group_hbox = hbox
        self.notebook.append_page(hbox, lbl)

        hbox = gtk.HBox(False)
        self.variable_liststore, self.variable_treeview = functions.make_treeview(
        (str, float, str), ['Variable Name', 'Weight', 'Levels'],
        editable=True, edit_handler=self.variable_cell_edited)
        hbox.pack_start(self.variable_treeview, True, True)
        vbuttonbox = gtk.VButtonBox()
        vbuttonbox.set_layout(gtk.BUTTONBOX_START)
        button = gtk.Button(None, gtk.STOCK_ADD)
        button.connect("clicked", self.variable_add_button)
        vbuttonbox.pack_start(button, False, False)
        button = gtk.Button(None, gtk.STOCK_DELETE)
        button.connect("clicked", self.variable_delete_button)
        vbuttonbox.pack_start(button, False, False)
        hbox.pack_start(vbuttonbox, False, False)
        lbl = gtk.Label('Variables')
        self.notbook_tabs.append("Variables: Define trial prognostic factors, their weights and levels")
        self.variable_hbox = hbox
        self.notebook.append_page(hbox, lbl)

        hbox = gtk.HBox(False)
        vbox = gtk.VBox(False)

        self.allocations_treeview = gtk.TreeView()
        hbox.pack_start(self.allocations_treeview, True, True)

        vbuttonbox = gtk.VButtonBox()
        vbuttonbox.set_layout(gtk.BUTTONBOX_START)
        button = gtk.Button(None, gtk.STOCK_ADD)
        button.connect("clicked", self.allocations_add_button)
        vbuttonbox.pack_start(button, False, False)
        button = gtk.Button(None, gtk.STOCK_UNDO)
        button.connect("clicked", self.allocations_undo_button)
        vbuttonbox.pack_start(button, False, False)
        vbox.pack_start(vbuttonbox, False, False)
        self.combo_vbox = gtk.VBox(False)
        vbox.pack_start(self.combo_vbox, False, False)
        hbox.pack_start(vbox, False, False)

        lbl = gtk.Label('Allocations')
        self.notbook_tabs.append('Allocations: Allocate subjects to treatments. To use minimization select "Minimize" as group name')
        self.notebook.append_page(hbox, lbl)

        vbox = gtk.VBox(False)
        self.freq_table_treeview = gtk.TreeView()
        vbox.pack_start(self.freq_table_treeview, False, False)
        self.initial_table_vbox = gtk.VBox(False)
        self.freq_table_enable_edit_button = gtk.Button("Start Edit")
        self.freq_table_enable_edit_button.connect("clicked", self.freq_table_start_edit_clicked)
        self.initial_table_vbox.pack_start(self.freq_table_enable_edit_button, False, False)
        button = gtk.Button("Delete Initial Table")
        button.connect("clicked", self.freq_table_delete_initial_clicked)
        self.initial_table_vbox.pack_start(button, False, False)
        vbox.pack_start(self.initial_table_vbox, False, False)
        lbl = gtk.Label('Table')
        self.notbook_tabs.append("Table: Displays frequency table for different levels of prognostic factors")
        self.notebook.append_page(vbox, lbl)

        hbox = gtk.HBox(True)
        treestore = gtk.TreeStore(str, float, float, float, float)
        self.balance_table_treeview = gtk.TreeView(treestore)
        col_names = ['Variable/Level', 'Marginal Balance', 'Range', 'Variance', 'SD']
        for i in range(5):
            cell = gtk.CellRendererText()
            column = gtk.TreeViewColumn(col_names[i], cell, text=i)
            self.balance_table_treeview.append_column(column)
        hbox.pack_start(self.balance_table_treeview, True, True)
        lbl = gtk.Label('Balance')
        self.notbook_tabs.append("Balance: Displays current trial balance as four different balance measures")
        self.notebook.append_page(hbox, lbl)

        hbox = gtk.HBox(True)
        self.about_text = gtk.TextView()
        self.about_text.set_wrap_mode(gtk.WRAP_WORD)
        self.about_text.set_editable(False)
        self.about_text.set_cursor_visible(False)
        self.about_text.set_left_margin(20)
        self.about_text.set_right_margin(20)
        about_text = """


*************************** MinimPy 0.1 ***********************************
Copyright (c) 2010 Mahmoud Saghaei
http://www.saghaei.com
MinimPy is a desktop minimization program for sequential randomization of subject to treatment groups in a clinical trial.
With this program you can define a new minimization model specifying the probability method used for assigning subject to treatment groups, distance measure which is the imbalance score at a the time of allocation considering the levels of prognostic factor for the a new subject which going to be allocated. Main features of MinimPy include total control over choice of probability method, distance measure, defining an initial minimization preload, adding a new method of probability assignment as biased coin minimization and another as marginal balance which has been shown to be superior to other distance measures. 
        """
        functions.set_text_buffer(self.about_text, about_text)
        hbox.pack_start(self.about_text)
        button = gtk.Button(None, gtk.STOCK_INFO)
        button.connect("clicked", self.license_button_clicked)
        functions.textview_add_widget(self.about_text, 0, button)
        self.notbook_tabs.append("ABout: Displays information about MinimPy and the developers")
        lbl = gtk.Label('ABout')
        self.notebook.append_page(hbox, lbl)
        self.notebook.set_show_tabs(True)

        self.window.add(win_vbox)
        self.window.show_all()
        self.initial_table_vbox.hide()

    def balance_table_refresh(self):
        liststore = self.freq_table_treeview.get_model()
        if not liststore:
            self.statusbar.pop(self.sb_context_id)
            self.statusbar.push(self.sb_context_id, "Error: No allocation!")
            return False
        table = []
        for row in range(len(liststore)-1):
            table.append([])
            for col in range(1, len(liststore[0])-1):
                table[row].append(liststore[row][col])
        balance = self.get_trial_balances(table)
        treestore = self.balance_table_treeview.get_model()
        treestore.clear()
        variables = [range(len(self.variable_liststore[row][2].split(','))) for row in range(len(self.variable_liststore))]
        row = 0
        for idx, variable in enumerate(variables):
            var_total = [0] * 4
            level_rows = []
            for level in variable:
                for i in range(4):
                    var_total[i] += balance[row][i]
                level_rows.append(balance[row])
                row += 1
            var_total = [self.variable_liststore[idx][0]] + [var_total[i] / len(variable) for i in range(4)]
            variable_node = treestore.append(None, var_total)
            level_names = map(str.strip, self.variable_liststore[idx][2].split(','))
            for level, level_row in enumerate(level_rows):
                treestore.append(variable_node, [level_names[level]] + level_row)
        treestore.append(None, ['Mean'] + balance[row])
        treestore.append(None, ['Max'] + balance[row+1])
        self.window.show_all()

    def get_trial_balances(self, table):
        model = Model()
        m = Minim(model)
        levels = [[] for col in table[0]]
        balances = [[] for col in table[0]] + [[], []]
        for row in table:
            for col, value in enumerate(row):
                levels[col].append(value)
        for row, level_count in enumerate(levels):
            allocation_ratio = [self.group_liststore[r][1] for r in range(len(self.group_liststore))]
            adj_count = [(1.0 * level_count[i]) / allocation_ratio[i] for i in range(len(level_count))]
            balances[row].append(m.get_marginal_balance(adj_count))
            balances[row].append(max(adj_count) - min(adj_count))
            balances[row].append(m.get_variance(adj_count))
            balances[row].append(m.get_standard_deviation(adj_count))
        for col in range(4):
             balances[len(levels)].append(1.0 * sum([balances[row][col] for row in range(len(levels))]) / len(levels))
             balances[len(levels)+1].append(max([balances[row][col] for row in range(len(levels))]))
        return balances

    def freq_table_disable_edit(self):
        cols = self.freq_table_treeview.get_columns()
        for n, col in enumerate(cols):
            cells = col.get_cell_renderers()
            for cell in cells:
                cell.set_property('editable', False)

    def freq_table_enable_edit(self):
        cols = self.freq_table_treeview.get_columns()
        for n, col in enumerate(cols):
            cells = col.get_cell_renderers()
            for cell in cells:
                cell.set_property('editable', True)
                cell.connect("edited", self.freq_table_treeview_edit_handler, n, len(cols))

    def freq_table_start_edit_clicked(self, button, data=False):
        if button.get_label() == "Start Edit":
            self.freq_table_enable_edit()
            button.set_label("Save as Initial Table")
        else:
            groups = range(len(self.group_liststore))
            variables = [range(len(self.variable_liststore[row][2].split(','))) for row in range(len(self.variable_liststore))]
            initial_freq_model = self.freq_table_treeview.get_model()
            table = [[[0 for l in v] for v in variables] for g in groups]
            for row, group in enumerate(table):
                col = 1
                for v, variable in enumerate(group):
                    for l, level in enumerate(variable):
                        table[row][v][l] = initial_freq_model[row][col]
                        col += 1
            self.initial_freq_table = table
            button.set_label("Start Edit")

    def freq_table_treeview_edit_handler(self, cell, row, new_text, col, cols):
        model = self.freq_table_treeview.get_model()
        if col == 0:
            self.statusbar.pop(self.sb_context_id)
            self.statusbar.push(self.sb_context_id, "Error: This cell can not be changed!")
            return False
        if not new_text.isdigit():
            self.statusbar.pop(self.sb_context_id)
            self.statusbar.push(self.sb_context_id, "Error: Only interger values allowed!")
            return False
        model[row][col] = int(new_text)

    def freq_table_delete_initial_clicked(self, button, data=None):
        self.initial_freq_table = False
        self.freq_table_disable_edit()
        self.freq_table_enable_edit_button.set_label("Start Edit")
        
    def load_trial_clicked(self, button, data=None):
        file_name = self.select_file("Select file", gtk.FILE_CHOOSER_ACTION_OPEN)
        if not file_name:
            self.statusbar.pop(self.sb_context_id)
            self.statusbar.push(self.sb_context_id, "Error: Trial load canceled!")
            return False
        self.trial_file_name = self.load_trial(file_name)

    def save_trial_clicked(self, button, data=None):
        if not self.trial_is_valid():
            self.statusbar.pop(self.sb_context_id)
            self.statusbar.push(self.sb_context_id, "Error: This trial is not ready to save! Please check the requirements")
            return False
        file_name = self.select_file("Enter name of the file", gtk.FILE_CHOOSER_ACTION_SAVE)
        if not file_name:
            self.statusbar.pop(self.sb_context_id)
            self.statusbar.push(self.sb_context_id, "Error: Trial save canceled!")
            return False
        self.build_ui_pool()
        self.trial_file_name = self.save_trial(file_name)

    def load_trial(self, file_name, lock=True):
        trial_state = self.trial_sample_size_spin.get_property("sensitive")
        temp_file = self.save_trial()
        try:
            fp = open(file_name, 'rb')
            data = pickle.load(fp)
            fp.close()
            self.ui_pool = data['ui_pool']
            self.trial_title_entry.set_text(data['trial_title'])
            self.trial_sample_size_spin.set_value(data['sample_size'])
            functions.set_text_buffer(self.trial_description_text, data['trial_description'])
            self.set_trial_properties(data['trial_properties'])
            self.high_prob_spin.set_value(data['high_prob'])
            self.initial_freq_table = data['initial_freq_table']
            self.prob_method_radios[data['prob_method']].set_active(True)
            self.distance_measure_radios[data['distance_measure']].set_active(True)
            self.allocations = data['allocations']
            self.set_groups_data(data['groups'])
            self.set_variables_data(data['variables'])
            self.update_allocations()
            if len(self.group_liststore):
                self.min_group_combo.set_active(data['min_group'])
            if lock:
                self.lock_trial()
                return file_name
        except Exception as ex:
            self.lock_trial(trial_state)
            self.load_trial(file_name=temp_file, lock=False)
            functions.error_dialog(self.window, 'An error occured while loading the trial!\nThe selected file may not be valid\n' + repr(ex) + '\nTrial restored to its previous state', 'Trial load failed!')
            return False

    def set_groups_data(self, groups):
        self.group_liststore.clear()
        for group in groups:
            self.group_liststore.append((group['name'], group['allocation_ratio']))

    def set_variables_data(self, variables):
        self.variable_liststore.clear()
        for variable in variables:
            self.variable_liststore.append((variable['name'], variable['weight'], variable['levels']))

    def build_ui_pool(self, lst=None):
        if not lst:
            ss = self.trial_sample_size_spin.get_value_as_int()
            lst = range(ss)
        random.shuffle(lst)
        self.ui_pool = map(lambda x: x.zfill(len(str(max(lst)))), map(str, lst))

    def get_trial_properties(self):
        table = [['', ''] for row in range(len(self.trial_properties_liststore))]
        for row in range(len(table)):
            for col in range(len(table[0])):
                table[row][col] = self.trial_properties_liststore[row][col]
        return table

    def set_trial_properties(self, table):
        self.trial_properties_liststore.clear()
        for row in range(len(table)):
            self.trial_properties_liststore.append(table[row])

    def trial_is_valid(self):
        if not self.trial_title_entry.get_text():
            functions.error_dialog(self.window, "Please specify the trial title first", "Trial title")
            self.notebook.set_current_page(0)
            self.window.set_focus(self.trial_title_entry)
            return False
        if not functions.get_text_buffer(self.trial_description_text):
            functions.error_dialog(self.window, "Please enter a description for this trial first", "Trial description")
            self.notebook.set_current_page(0)
            self.window.set_focus(self.trial_description_text)
            return False
        if len(self.group_liststore) < 2:
            functions.error_dialog(self.window, "At least two groups must be defined for the trial", "Trial groups")
            self.notebook.set_current_page(1)
            self.window.set_focus(self.group_treeview)
            return False
        if len(self.variable_liststore) < 1:
            functions.error_dialog(self.window, "At least one prognostic factor (variable) must be defined for the trial", "Trial variables")
            self.notebook.set_current_page(2)
            self.window.set_focus(self.variable_treeview)
            return False
        msg = "Are you sure you want to save the trial?\nOnce you saved a trial no change in trial settings are allowed!\nTrial details:\n" + self.get_trial_info()
        dialog = gtk.MessageDialog(self.window, flags = gtk.DIALOG_MODAL, type = gtk.MESSAGE_QUESTION, buttons=gtk.BUTTONS_YES_NO, message_format = msg)
        dialog.set_title("Trial Save!")
        if dialog.run() == gtk.RESPONSE_YES:
            dialog.destroy()
            return True
        dialog.destroy()
        return False

    def want_trial_unlock(self):
        msg = "Do you want to unlock the trial!\nUnlocking the trial make changes of trial setting possible!\nTrial file will be released.\nYou have to save the trial again!"
        dialog = gtk.MessageDialog(self.window, flags = gtk.DIALOG_MODAL, type = gtk.MESSAGE_QUESTION, buttons=gtk.BUTTONS_YES_NO, message_format = msg)
        dialog.set_title("Unlock Trial!")
        if dialog.run() == gtk.RESPONSE_YES:
            self.lock_trial(True)
            self.ui_pool = None
            self.trial_file_name = None
        dialog.destroy()

    def lock_trial(self, state=False):
        self.trial_load_button.set_property("sensitive", state)
        self.trial_save_button.set_property("sensitive", state)
        self.trial_sample_size_spin.set_property("sensitive", state)
        for radio in self.prob_method_radios:
            radio.set_property("sensitive", state)
        self.min_group_combo.set_property("sensitive", state)
        self.high_prob_spin.set_property("sensitive", state)
        for radio in self.distance_measure_radios:
            radio.set_property("sensitive", state)
        for child in self.group_hbox.get_children():
            child.set_property("sensitive", state)
        for child in self.variable_hbox.get_children():
            child.set_property("sensitive", state)
        for child in self.initial_table_vbox.get_children():
            child.set_property("sensitive", state)
        self.statusbar.pop(self.sb_context_id)
        self.statusbar.push(self.sb_context_id, ("Trial lock!", "Trial unlocked!")[state])

    def get_trial_info(self):
        ret = []
        ret.append("Title: " + self.trial_title_entry.get_text())
        ret.append("Sample Size: " + str(self.trial_sample_size_spin.get_value_as_int()))
        ret.append("Description: " + functions.get_text_buffer(self.trial_description_text))
        ret.append("Probability Method: " + self.get_prob_method_name())
        ret.append("Distance Measure: " + self.get_distance_measure_name())
        ret.append("High Probability: " + str(self.high_prob_spin.get_value()))
        ret.append("Groups: " + str(self.get_groups_data()))
        ret.append("Variables: " + str(self.get_variables_data()))
        return '\n'.join(ret)

    def save_trial(self, file_name=None):
        if file_name:
            # do not lock if this is a temporary save
            self.lock_trial()
        else:
            file_name = tempfile.mkstemp()[1]
        try:
            data = {}
            data['ui_pool'] = self.ui_pool
            data['trial_title'] = self.trial_title_entry.get_text()
            data['sample_size'] = self.trial_sample_size_spin.get_value_as_int()
            data['trial_description'] = functions.get_text_buffer(self.trial_description_text)
            data['trial_properties'] = self.get_trial_properties()
            data['high_prob'] = self.high_prob_spin.get_value()
            data['min_group'] = self.min_group_combo.get_active()
            data['initial_freq_table'] = self.initial_freq_table
            data['prob_method'] = self.get_prob_method()
            data['distance_measure'] = self.get_distance_measure()
            data['allocations'] = self.allocations
            data['groups'] = self.get_groups_data()
            data['variables'] = self.get_variables_data()
            fp = open(file_name, 'wb')
            pickle.dump(data, fp)
            fp.close()
            # useful if temp save
            return file_name
        except Exception as ex:
            functions.error_dialog(self.window, "An unknown error occured during trial save!\n" + repr(ex), "Trial save error!")
            return False

    def get_distance_measure_name(self):
        for distance_measure_radio in self.distance_measure_radios:
            if distance_measure_radio.get_active():
                return distance_measure_radio.get_label()
        return "Unknown"

    def get_distance_measure(self):
        for idx, distance_measure_radio in enumerate(self.distance_measure_radios):
            if distance_measure_radio.get_active():
                return idx
        return 0

    def get_prob_method_name(self):
        for prob_method_radio in self.prob_method_radios:
            if prob_method_radio.get_active():
                return prob_method_radio.get_label()
        return "Unknown"

    def get_prob_method(self):
        for idx, prob_method_radio in enumerate(self.prob_method_radios):
            if prob_method_radio.get_active():
                return idx
        return 0

    def get_variables_data(self):
        variables = []
        for row in self.variable_liststore:
            variable = {}
            variable['name'] = row[0]
            variable['weight'] = row[1]
            variable['levels'] = row[2]
            variables.append(variable)
        return variables

    def get_groups_data(self):
        groups = []
        for row in self.group_liststore:
            group = {}
            group['name'] = row[0]
            group['allocation_ratio'] = row[1]
            groups.append(group)
        return groups

    def select_file(self, title, action):
        """
        A generic method returning file name
        """
        stock = {gtk.FILE_CHOOSER_ACTION_OPEN: gtk.STOCK_OPEN, gtk.FILE_CHOOSER_ACTION_SAVE: gtk.STOCK_SAVE}
        buttons = (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, stock[action], gtk.RESPONSE_OK)
        fdialog = gtk.FileChooserDialog(title, self.window, action, buttons)
        if action == gtk.FILE_CHOOSER_ACTION_SAVE:
            fdialog.set_do_overwrite_confirmation(True)
        fdialog.set_default_response(gtk.RESPONSE_OK)
        response = fdialog.run()
        if response == gtk.RESPONSE_OK:
            file_name = fdialog.get_filename()
        else:
            file_name = None
        fdialog.destroy()
        return file_name

def main():
    gtk.main()
    return 0

if __name__ == "__main__":
    ModelInteface()
    main()

