# Based on iwidgets2.2.0/entryfield.itk code.

import re
import string
import types
import tkinter
import Pmw

# Possible return values of validation functions.
OK = 1
ERROR = 0
PARTIAL = -1

class EntryField(Pmw.MegaWidget):
    _classBindingsDefinedFor = 0

    def __init__(self, parent = None, **kw):

        # Define the megawidget options.
        INITOPT = Pmw.INITOPT
        optiondefs = (
            ('command',           None,        None),
            ('errorbackground',   'pink',      None),
            ('invalidcommand',    self.bell,   None),
            ('labelmargin',       0,           INITOPT),
            ('labelpos',          None,        INITOPT),
            ('modifiedcommand',   None,        None),
            ('sticky',            'ew',        INITOPT),
            ('validate',          None,        self._validate),
            ('extravalidators',   {},          None),
            ('value',             '',          INITOPT),
        )
        self.defineoptions(kw, optiondefs)

        # Initialise the base class (after defining the options).
        Pmw.MegaWidget.__init__(self, parent)

        # Create the components.
        interior = self.interior()
        self._entryFieldEntry = self.createcomponent('entry',
                (), None,
                tkinter.Entry, (interior,))
        self._entryFieldEntry.grid(column=2, row=2, sticky=self['sticky'])
        if self['value'] != '':
            self.__setEntry(self['value'])
        interior.grid_columnconfigure(2, weight=1)
        interior.grid_rowconfigure(2, weight=1)

        self.createlabel(interior)

        # Initialise instance variables.

        self.normalBackground = None
        self._previousText = None

        # Initialise instance.

        _registerEntryField(self._entryFieldEntry, self)

        # Establish the special class bindings if not already done.
        # Also create bindings if the Tkinter default interpreter has
        # changed.  Use Tkinter._default_root to create class
        # bindings, so that a reference to root is created by
        # bind_class rather than a reference to self, which would
        # prevent object cleanup.
        if EntryField._classBindingsDefinedFor != tkinter._default_root:
            tagList = self._entryFieldEntry.bindtags()
            root  = tkinter._default_root

            allSequences = {}
            for tag in tagList:

                sequences = root.bind_class(tag)
                if type(sequences) is str:
                    # In old versions of Tkinter, bind_class returns a string
                    sequences = root.tk.splitlist(sequences)

                for sequence in sequences:
                    allSequences[sequence] = None
            for sequence in list(allSequences.keys()):
                root.bind_class('EntryFieldPre', sequence, _preProcess)
                root.bind_class('EntryFieldPost', sequence, _postProcess)

            EntryField._classBindingsDefinedFor = root

        self._entryFieldEntry.bindtags(('EntryFieldPre',) +
                self._entryFieldEntry.bindtags() + ('EntryFieldPost',))
        self._entryFieldEntry.bind('<Return>', self._executeCommand)

        # Check keywords and initialise options.
        self.initialiseoptions()

    def destroy(self):
        _deregisterEntryField(self._entryFieldEntry)
        Pmw.MegaWidget.destroy(self)

    def _getValidatorFunc(self, validator, index):
        # Search the extra and standard validator lists for the
        # given 'validator'.  If 'validator' is an alias, then
        # continue the search using the alias.  Make sure that
        # self-referencial aliases do not cause infinite loops.

        extraValidators = self['extravalidators']
        traversedValidators = []

        while 1:
            traversedValidators.append(validator)
            if validator in extraValidators:
                validator = extraValidators[validator][index]
            elif validator in _standardValidators:
                validator = _standardValidators[validator][index]
            else:
                return validator
            if validator in traversedValidators:
                return validator

    def _validate(self):
        dictio = {
            'validator' : None,
            'min' : None,
            'max' : None,
            'minstrict' : 1,
            'maxstrict' : 1,
        }
        opt = self['validate']
        if type(opt) is dict:
            dictio.update(opt)
        else:
            dictio['validator'] = opt

        # Look up validator maps and replace 'validator' field with
        # the corresponding function.
        validator = dictio['validator']
        valFunction = self._getValidatorFunc(validator, 0)
        self._checkValidateFunction(valFunction, 'validate', validator)
        dictio['validator'] = valFunction

        # Look up validator maps and replace 'stringtovalue' field
        # with the corresponding function.
        if 'stringtovalue' in dictio:
            stringtovalue = dictio['stringtovalue']
            strFunction = self._getValidatorFunc(stringtovalue, 1)
            self._checkValidateFunction(
                    strFunction, 'stringtovalue', stringtovalue)
        else:
            strFunction = self._getValidatorFunc(validator, 1)
            if strFunction == validator:
                strFunction = len
        dictio['stringtovalue'] = strFunction

        self._validationInfo = dictio
        args = dictio.copy()
        del args['validator']
        del args['min']
        del args['max']
        del args['minstrict']
        del args['maxstrict']
        del args['stringtovalue']
        self._validationArgs = args
        self._previousText = None

        if type(dictio['min']) is str and strFunction is not None:
            dictio['min'] = strFunction(*(dictio['min'],), **args)
        if type(dictio['max']) is str and strFunction is not None:
            dictio['max'] = strFunction(*(dictio['max'],), **args)

        self._checkValidity()

    def _checkValidateFunction(self, function, option, validator):
        # Raise an error if 'function' is not a function or None.

        if function is not None and not callable(function):
            extraValidators = self['extravalidators']
            extra = list(extraValidators.keys())
            extra.sort()
            extra = tuple(extra)
            standard = list(_standardValidators.keys())
            standard.sort()
            standard = tuple(standard)
            msg = 'bad %s value "%s":  must be a function or one of ' \
                'the standard validators %s or extra validators %s'
            raise ValueError(msg % (option, validator, standard, extra))

    def _executeCommand(self, event = None):
        cmd = self['command']
        if callable(cmd):
            if event is None:
                # Return result of command for invoke() method.
                return cmd()
            else:
                cmd()

    def _preProcess(self):

        self._previousText = self._entryFieldEntry.get()
        self._previousICursor = self._entryFieldEntry.index('insert')
        self._previousXview = self._entryFieldEntry.index('@0')
        if self._entryFieldEntry.selection_present():
            self._previousSel= (self._entryFieldEntry.index('sel.first'),
                self._entryFieldEntry.index('sel.last'))
        else:
            self._previousSel = None

    def _postProcess(self):

        # No need to check if text has not changed.
        previousText = self._previousText
        if previousText == self._entryFieldEntry.get():
            return self.valid()

        valid = self._checkValidity()
        if self.hulldestroyed():
            # The invalidcommand called by _checkValidity() destroyed us.
            return valid

        cmd = self['modifiedcommand']
        if callable(cmd) and previousText != self._entryFieldEntry.get():
            cmd()
        return valid

    def checkentry(self):
        # If there is a variable specified by the entry_textvariable
        # option, checkentry() should be called after the set() method
        # of the variable is called.

        self._previousText = None
        return self._postProcess()

    def _getValidity(self):
        text = self._entryFieldEntry.get()
        dictio = self._validationInfo
        args = self._validationArgs

        if dictio['validator'] is not None:
            status = dictio['validator'](*(text,), **args)
            if status != OK:
                return status

        # Check for out of (min, max) range.
        if dictio['stringtovalue'] is not None:
            min = dictio['min']
            max = dictio['max']
            if min is None and max is None:
                return OK
            val = dictio['stringtovalue'](*(text,), **args)
            if min is not None and val < min:
                if dictio['minstrict']:
                    return ERROR
                else:
                    return PARTIAL
            if max is not None and val > max:
                if dictio['maxstrict']:
                    return ERROR
                else:
                    return PARTIAL
        return OK

    def _checkValidity(self):
        valid = self._getValidity()
        oldValidity = valid

        if valid == ERROR:
            # The entry is invalid.
            cmd = self['invalidcommand']
            if callable(cmd):
                cmd()
            if self.hulldestroyed():
                # The invalidcommand destroyed us.
                return oldValidity

            # Restore the entry to its previous value.
            if self._previousText is not None:
                self.__setEntry(self._previousText)
                self._entryFieldEntry.icursor(self._previousICursor)
                self._entryFieldEntry.xview(self._previousXview)
                if self._previousSel is not None:
                    self._entryFieldEntry.selection_range(self._previousSel[0],
                        self._previousSel[1])

                # Check if the saved text is valid as well.
                valid = self._getValidity()

        self._valid = valid

        if self.hulldestroyed():
            # The validator or stringtovalue commands called by
            # _checkValidity() destroyed us.
            return oldValidity

        if valid == OK:
            if self.normalBackground is not None:
                self._entryFieldEntry.configure(
                        background = self.normalBackground)
                self.normalBackground = None
        else:
            if self.normalBackground is None:
                self.normalBackground = self._entryFieldEntry.cget('background')
                self._entryFieldEntry.configure(
                        background = self['errorbackground'])

        return oldValidity

    def invoke(self):
        return self._executeCommand()

    def valid(self):
        return self._valid == OK

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

    def __setEntry(self, text):
        oldState = str(self._entryFieldEntry.cget('state'))
        if oldState != 'normal':
            self._entryFieldEntry.configure(state='normal')
        self._entryFieldEntry.delete(0, 'end')
        self._entryFieldEntry.insert(0, text)
        if oldState != 'normal':
            self._entryFieldEntry.configure(state=oldState)

    def setentry(self, text):
        self._preProcess()
        self.__setEntry(text)
        return self._postProcess()

    def getvalue(self):
        return self._entryFieldEntry.get()

    def setvalue(self, text):
        return self.setentry(text)

Pmw.forwardmethods(EntryField, tkinter.Entry, '_entryFieldEntry')

# ======================================================================


# Entry field validation functions

_numericregex = re.compile('^[0-9]*$')
_alphabeticregex = re.compile('^[a-z]*$', re.IGNORECASE)
_alphanumericregex = re.compile('^[0-9a-z]*$', re.IGNORECASE)

def numericvalidator(text):
    if text == '':
        return PARTIAL
    else:
        if _numericregex.match(text) is None:
            return ERROR
        else:
            return OK

def integervalidator(text):
    if text in ('', '-', '+'):
        return PARTIAL
    try:
        int(text)
        return OK
    except ValueError:
        return ERROR

def alphabeticvalidator(text):
    if _alphabeticregex.match(text) is None:
        return ERROR
    else:
        return OK

def alphanumericvalidator(text):
    if _alphanumericregex.match(text) is None:
        return ERROR
    else:
        return OK

def hexadecimalvalidator(text):
    if text in ('', '0x', '0X', '+', '+0x', '+0X', '-', '-0x', '-0X'):
        return PARTIAL
    try:
        int(text, 16)
        return OK
    except ValueError:
        return ERROR

def realvalidator(text, separator = '.'):
    if separator != '.':
        #Py3 if string.find(text, '.') >= 0:
        if text.find('.') >= 0:
            return ERROR
        #Py3 index = string.find(text, separator)
        index = text.find(separator)
        if index >= 0:
            text = text[:index] + '.' + text[index + 1:]
    try:
        float(text)
        return OK
    except ValueError:
        # Check if the string could be made valid by appending a digit
        # eg ('-', '+', '.', '-.', '+.', '1.23e', '1E-').
        if len(text) == 0:
            return PARTIAL
        if text[-1] in string.digits:
            return ERROR
        try:
            float(text + '0')
            return PARTIAL
        except ValueError:
            return ERROR

def timevalidator(text, separator = ':'):
    try:
        Pmw.timestringtoseconds(text, separator)
        return OK
    except ValueError:
        if len(text) > 0 and text[0] in ('+', '-'):
            text = text[1:]
        if re.search('[^0-9' + separator + ']', text) is not None:
            return ERROR
        return PARTIAL

def datevalidator(text, fmt = 'ymd', separator = '/'):
    try:
        Pmw.datestringtojdn(text, fmt, separator)
        return OK
    except ValueError:
        if re.search('[^0-9' + separator + ']', text) is not None:
            return ERROR
        return PARTIAL

_standardValidators = {
    'numeric'      : (numericvalidator,      int),
    'integer'      : (integervalidator,      int),
    'hexadecimal'  : (hexadecimalvalidator,  lambda s: int(s, 16)),
    'real'         : (realvalidator,         Pmw.stringtoreal),
    'alphabetic'   : (alphabeticvalidator,   len),
    'alphanumeric' : (alphanumericvalidator, len),
    'time'         : (timevalidator,         Pmw.timestringtoseconds),
    'date'         : (datevalidator,         Pmw.datestringtojdn),
}

_entryCache = {}

def _registerEntryField(entry, entryField):
    # Register an EntryField widget for an Entry widget

    _entryCache[entry] = entryField

def _deregisterEntryField(entry):
    # Deregister an Entry widget
    del _entryCache[entry]

def _preProcess(event):
    # Forward preprocess events for an Entry to it's EntryField

    _entryCache[event.widget]._preProcess()

def _postProcess(event):
    # Forward postprocess events for an Entry to it's EntryField

    # The function specified by the 'command' option may have destroyed
    # the megawidget in a binding earlier in bindtags, so need to check.
    if event.widget in _entryCache:
        _entryCache[event.widget]._postProcess()
