#
# hwWx/mixin.py: define data dialogs for use in GUI-based applications
#

tpINT = 1  # Integer
tpTXT = 2  # Text
tpCHC = 3  # Choice (string)
tpICH = 4  # Indirect choice (?)
tpSPN = 5  # Spin Control
tpNTX = 6  # Nullable text
tpNCH = 7  # Nullable choice
tpFNT = 8  # Font
tpCOL = 9  # Colo(u)r value - RGB
tpSTX = 10 # Static text
tpSTN = 11 # Static text (nullable)
tpSIN = 12 # Static integer (nullable)
tpSDN = 13 # Static date (nullable)
tpSTS = 14 # Static timestamp (nullable)
tpCHK = 15 # Checkbox

#
# Ideally this code will be refactored to allow each field type to be
# represented by a class, whose methods can be called to achieve the
# various actions and values required. At the moment everything is
# rather procedural.
#
__all__ = ('setPmark', 'hwDataDialog', 'MsgDlg', 'fontString',
           'tpINT', 'tpTXT', 'tpCHC', 'tpICH', 'tpSPN', 'tpNTX',
           'tpNCH', 'tpFNT', 'tpCOL', 'tpSTX', 'tpSTN', 'tpSIN',
           'tpSDN', 'tpSTS', 'tpCHK')

import wx
import time, datetime

def setPmark(p):
    global pmark
    pmark = p

def checkPmark():
    try:
        pmark
    except NameError:
        raise RuntimeError("Paramstyle not set: need call to setPmark()")

class hwDataDialog:
    """Mixin for wx dialogs.
    The class definition is exprected to provide the followinf definitions.

    FIELDS:         a list of field descriptions.
    Each description is of the form (wxID, fieldname, fieldtype)
        where       wxID identifies the window control representation
                    fieldname is the name of the column in the table
                    fieldtype is one of the tpXXX codes defined above
            
    table           the name of the database table to be maintained
    keyfield        the name of the primary key column
    uniquefields    an iterable identifying fields that must be unique
    selfields       None or name of the database column used to select rows
                    for editing
    selector        None or the wx ID of the control displaying the available
                    values of the selfield"""
    #
    # Row handling methods
    #
    def EmptyRow(self):
        """Returns a row of empty columns."""
        row = {}
        for fld, col, typ in self.FIELDS:
            if typ in (tpTXT, tpCHC, tpICH, tpFNT, tpSTX):
                val = ""
            elif typ in (tpNTX, tpNCH, tpSTN, tpSIN, tpSDN, tpSTS):
                val = None
            elif typ == tpCOL:
                val = (0, 0, 0)
            elif typ in (tpSPN, tpINT, tpCHK):
                val = 0
            else:
                raise ValueError("Illegal field type value %s" % typ)
            row[col] = val
        return row

    def GetContents(self):
        """Returns a row containing the dialog's current contents."""
        row = {}
        for fld, col, typ in self.FIELDS:
            item = self.FindWindowById(fld)
            if typ in (tpTXT, tpNTX):
                val = item.GetValue().encode('utf-8')
                if typ == tpNTX and val == "":
                    val = None
            elif typ in (tpCHC, tpNCH):
                val = item.GetStringSelection()
                if typ == tpNCH and item.GetSelection() == 0:
                    val = None
            elif typ == tpINT:
                val = int(item.GetValue() or 0)
            elif typ == tpICH:
                val = item.GetStringSelection()
                val = item.hwFwdLookup[val]
            elif typ == tpSPN:
                val = item.GetValue()
            elif typ == tpFNT:
                val = item.hwFontVal
            elif typ == tpCOL:
                val = str(item.GetBackgroundColour())
            elif typ in (tpSTX, tpSTN):
                val = item.GetLabel()
                if typ == tpSTN and val == "":
                    val = None
            elif typ == tpSIN:
                label = item.GetLabel()
                if label == u"None":
                    val = None
                else:
                    val = int(label)
            elif typ == tpSDN:
                s = item.GetLabel()
                if s == u"":
                    val = None
                else:
                    val = datetime.date.fromtimestamp(time.mktime(time.strptime(s, "%d-%b-%Y")))
            elif typ == tpSTS:
                s = item.GetLabel()
                if s == u"":
                    val = None
                else:
                    val = datetime.datetime.fromtimestamp(time.mktime(time.strptime(s, "%d-%b-%Y %H:%M:%S")))
            elif typ == tpCHK:
                if item.IsChecked():
                    val = 1
                else:
                    val = 0
            else:
                raise ValueError("Illegal field type value %s" % typ)
            row[col] = val
        return row

    def SetContents(self, row):
        """Establishes the content of the dialog from a given row."""
        for fld, col,typ in self.FIELDS:
            item = self.FindWindowById(fld)
            val = row[col]
            if typ == tpICH:
                if val:
                    item.SetStringSelection(item.hwRevLookup[val])
                else:
                    item.SetStringSelection("*None*")
            elif typ in (tpCHC, tpNCH):
                if val:
                    item.SetStringSelection(val)
                else:
                    item.SetSelection(0)
            elif typ == tpINT:
                item.SetValue(str(val))
            elif typ in (tpTXT, tpNTX): # text field needing only stringType coercion
                if val is None:
                    item.SetValue("")
                else:
                    item.SetValue(str(val))
            elif typ == tpSPN:
                item.SetValue(val)
            elif typ == tpFNT:
                item.hwFontVal = val
                item.SetLabel(fontString(val))
            elif typ == tpCOL:
                item.SetLabel("")
                if not val:
                    val = "(255, 255, 255)"
                item.SetBackgroundColour(eval(val))
            elif typ in (tpSTX, tpSTN):
                if typ == tpSTN and val is None:
                    item.SetLabel("")
                else:
                    item.SetLabel(str(val))
            elif typ == tpSIN:
                if val is None:
                    item.SetLabel("None")
                else:
                    item.SetLabel(str(val))
            elif typ == tpSDN:
                if val is not None:
                    item.SetLabel(val.strftime("%d-%b-%Y"))
                else:
                    item.SetLabel("")
            elif typ == tpSTS:
                if val is not None:
                    item.SetLabel(val.strftime("%d-%b-%Y %H:%M:%S"))
                else:
                    item.SetLabel("")
            elif typ == tpCHK:
                item.SetValue(val != 0)
            else:
                raise ValueError("Illegal field type value %s" % typ)

    #
    # Database utility methods
    #
    def readRow(self, id, keyfield, table):
        """Reads a row from the database."""
        checkPmark()
        sql = ("SELECT %s FROM %s WHERE %s=%s" %
               (",".join(f[1] for f in self.FIELDS), table, keyfield, pmark))
        self.curs.execute(sql, (id, ))
        return self.curs.fetchone()
        
    def deleteRow(self, id):
        checkPmark()
        self.DeleteHook(id)
        self.curs.execute("DELETE FROM %s WHERE %s=%s" % (self.table, self.keyfield, pmark), (id, ))
        self.conn.commit()
        return id

    def DeleteHook(self, id):
        """Override to avoid integrity violations by deleting related records."""
        pass

    def InsertRow(self, row, keyfield, table):
        """Inserts a new row into the database if it passes uniquenesss checks."""
        checkPmark()
        if self.CheckUnique(row):
                sql = ("INSERT INTO %s (%s) VALUES (%s)" %
                   (table, ", ".join(("%s" % f[1] for f in self.FIELDS)), ", ".join((pmark for f in self.FIELDS))))
                data = tuple([row[f[1]] for f in self.FIELDS])
                self.curs.execute(sql, data)
                # XXX: Database platform dependent
                # XXX: PostgreSQL:
                self.curs.execute("SELECT currval('%s_%s_seq')" % (table, keyfield))
                # XXX: Access 2000/SQL Server
                # self.curs.execute("SELECT @@IDENTITY")
                id = self.curs.fetchone()[0]
                row[keyfield] = id
                self.conn.commit()
                return id

    def InsertRowIfAsk(self, row, keyfield, table):
        if MsgDlg(self, "Do you want to insert this new row?", "Confirm Insertion") == wx.ID_YES:
            return self.InsertRow(row, keyfield, table)

    def UpdateRow(self, row, keyfield, table):
        """Updates an existing row in the database if it passes uniqueness checks.""" 
        checkPmark()
        if self.CheckUnique(row, self.ID):
            sql = ("UPDATE %s SET %s WHERE %s=%s" %
                   (table, ", ".join("%s=%s" % (f[1], pmark) for f in self.FIELDS), keyfield, pmark))
            data = tuple([row[f[1]] for f in self.FIELDS]) + (self.ID, )
            self.curs.execute(sql, data)
            self.conn.commit()
            return self.ID

    def UpdateRowIfAsk(self, row, keyfield, table):
        if self.row != row:
            if MsgDlg(self, "Do you want to update this row?", "Confirm Update") == wx.ID_YES:
                return self.UpdateRow(row, keyfield, table)

    def CheckUnique(self, row, id=None):
        """Verifies that all uniqueness constraints are met by current content."""
        sql = ("SELECT COUNT(*) FROM %s WHERE (%s)" %
               (self.table, " OR ".join("%s=%s" % (fld, pmark) for fld in self.uniquefields)))
        data = tuple(row[fld] for fld in self.uniquefields)
        if id:
            sql = "%s AND %s<>%s" % (sql, self.keyfield, pmark)
            data = data + (id, )
        self.curs.execute(sql, data)
        count, = self.curs.fetchone()
        if count==0:
            return True
        else:
            flds = []
            for fld in self.uniquefields:
                self.curs.execute("SELECT COUNT(*) FROM %s WHERE %s=%s" % (self.table, fld, pmark), (row[fld], ))
                if self.curs.fetchone() != (0, ):
                    flds.append(fld)
            MsgDlg(self, "The following fields contain duplicate values:\n %s" % ", ".join(flds).center(25), "Database Uniqueness Requirements Not Met", style=wx.OK)

    #
    # Selector utility methods
    #
    def SelectorLoad(self, selected=None):
        """Load the selector with data allowing selection of relevant items.
        
        This specific code assumes a specific interface for the selector item.
        XXX Really must take care of the "empty contents" case, since it crashes!"""
        names, vals = self.SelectorKeys()
        item = self.FindWindowById(self.selector)
        item.Clear() ## ControlWithItems
        if names:
            for name in names:
                item.Append(name)
            self.rowNames = dict(zip(names, vals))# Indexes to ID by path
            if selected:
                # XXX this appears to fail after a new addition
                item.SetStringSelection(selected) ## ControlWithItems
                return self.rowNames[selected]
            else:
                item.SetSelection(0)
                return self.rowNames[item.GetStringSelection()]

    def Deselect(self):
        """Put selector in a "nothing selected" state during new item entry."""
        item = self.FindWindowById(self.selector)
        item.SetSelection(wx.NOT_FOUND) ## ControlWithItems

    #
    # GUI utility methods
    #
    def fkByCol(self, query, control, NoneOption=None):
        self.curs.execute(query)
        NameId = self.curs.fetchall()
        control.Clear()
        if NoneOption:
            NameId.insert(0, NoneOption)
        return self.fkByColData(control, NameId)

    def fkByColData(self, control, NameId):
        for n, i in NameId:
            control.Append(n)
        control.hwFwdLookup = dict((unicode(x), y) for (x, y) in NameId)
        control.hwRevLookup = dict((y, x) for (x, y) in NameId)

    #
    # Event handlers
    #
    def OnClose(self, event):
        if self.newrecord:
            self.InsertRowIfAsk(self.GetContents(), self.keyfield, self.table)
        else:
            self.UpdateRowIfAsk(self.GetContents(), self.keyfield, self.table)
        self.EndModal(wx.ID_OK)

    def OnDelete(self, event):
        if not self.newrecord:
            res = MsgDlg(self, "Do you really want to delete record '%s'" % self.FindWindowById(self.selector).GetStringSelection(),
                            "Confirm deletion")
            if res == wx.ID_YES:
                # XXX should catch exceptions here due to (e.g.) constraint violations
                self.deleteRow(self.ID)
        else:
            res = MsgDlg(self, "Do you really want to delete this new record?", "Confirm deletion")
            if res != wx.ID_YES:
                return
        self.ID = self.SelectorLoad()
        if self.ID: # There may not *be* any records ...
            self.row = dict(zip(
                [f[1] for f in self.FIELDS],
                self.readRow(self.ID, self.keyfield, self.table)))
            self.SetContents(self.row)
        self.newrecord = False

    def OnDone(self, event):
        """Close window after any required database activity."""
        if self.newrecord:
            self.InsertRowIfAsk(self.GetContents(), self.keyfield, self.table)
        else:
            self.UpdateRowIfAsk(self.GetContents(), self.keyfield, self.table)
        self.EndModal(wx.ID_OK)

    def OnNew(self, event):
        """Handles creation of a new row."""
        if self.newrecord:
            id = self.InsertRowIfAsk(self.GetContents(), self.keyfield, self.table)
            self.SelectorLoad()
        else:
            id = self.UpdateRowIfAsk(self.GetContents(), self.keyfield, self.table)
        self.ID = None
        self.SetContents(self.EmptyRow())
        self.Deselect()
        self.newrecord = True

    def OnSave(self, event):
        """Write current window data to database if changed, without asking."""
        if self.newrecord:
            self.ID = self.InsertRow(self.GetContents(), self.keyfield, self.table)
            self.row = self.GetContents()
            self.SelectorLoad(self.row[self.selfield])
        else:
            row = self.GetContents()
            self.UpdateRow(row, self.keyfield, self.table)
            self.row = row
        self.newrecord = False

    def OnSelect(self, event):
        """Handles selection of an item.
        
        Reads in relevant table row and updates display.
        """
        # Check for existing dirty row and allow user to select writeback if required
        if self.newrecord:
            self.ID = self.InsertRowIfAsk(self.GetContents(), self.keyfield, self.table)
            self.SelectorLoad(self.row[self.selfield])
        else:
            self.UpdateRowIfAsk(self.GetContents(), self.keyfield, self.table)
        item = self.FindWindowById(self.selector)
        sel = item.GetStringSelection()
        self.ID = id = self.rowNames[sel]
        row = self.readRow(id, keyfield=self.keyfield, table=self.table)
        self.row = dict(zip((f[1] for f in self.FIELDS), row))
        self.SetContents(self.row)
        self.newrecord = False


def MsgDlg(parent, msg, ttl, style=wx.YES_NO | wx.YES_DEFAULT):
    dlg = wx.MessageDialog(parent, msg, ttl, style=style)
    res = dlg.ShowModal()
    dlg.Destroy()
    return res

def fontString(val):
    """Returns a readable string description of a font."""
    if not val:
        return ""
    return val
