"""gui entry box with data validation
    .validate(): validate control data
        if validation error ocuurs, entry box is truned red. If the entry box is safe, it is turned white.
        this function returns (stat, reason), where,
        stat is validation result (True or False)
        reason is structured error messeage which would be parsed by error.walk_errorstruct()

    .get() return control variables. it may cause exception.
    it would be recommended to run .validate() befor get()
"""

from __future__ import print_function, unicode_literals

import tkinter as tk


class ValidateAbstract(object):
    def validate(self):
        """validate control value
        @return: errorstruct defined at error.py
        """
        return None


class Int(tk.Entry):
    def __init__(self, master=None, textvariable=None, cnf={}, **kw):
        """
        @param textvariable: tkinter.IntVar object watched by this widget.
               If not given, variable object is generate inside the class.
        """
        tk.Entry.__init__(self, master, cnf, bg='white', **kw)
        if textvariable:
            self.cv = textvariable
        else:
            self.cv = tk.IntVar(master)
        self.config(textvariable=self.cv)

    def set(self, v):
        self.cv.set(v)

    def get(self):
        return self.cv.get()

    def get_nostatechk(self):
        return self.get()

    def validate(self):
        reason = None
        if self.is_disabled():
            return reason

        try:
            self.cv.get()
        except ValueError:
            reason = 'ValueError (should be Int)'

        if reason:
            self.config(bg='red')
        else:
            self.config(bg='white')
        return reason

    def enable(self):
        self.config(state=tk.NORMAL)

    def disable(self):
        self.config(state=tk.DISABLED)

    def is_disabled(self):
        return self.cget('state') == tk.DISABLED


class Double(tk.Entry):
    def __init__(self, master=None, textvariable=None, *args, **kw):
        """
        @param textvariable: tkinter.DoubleVar value watched by this widget.
               If not given, variable object is generate inside the class.
        """
        tk.Entry.__init__(self, master, bg='white', *args, **kw)
        if textvariable:
            self.cv = textvariable
        else:
            self.cv = tk.DoubleVar(self)
        self.config(textvariable=self.cv)

    def set(self, v):
        self.cv.set(v)

    def get(self):
        return self.cv.get()

    def get_nostatechk(self):
        return self.get()

    def validate(self):
        reason = None
        if self.is_disabled():
            return None
        try:
            self.cv.get()
        except ValueError:
            reason = 'ValueError (should be Double)'

        if reason:
            self.config(bg='red')
        else:
            self.config(bg='white')
        return reason

    def enable(self):
        self.config(state=tk.NORMAL)

    def disable(self):
        self.config(state=tk.DISABLED)

    def is_disabled(self):
        return self.cget('state') == tk.DISABLED


class IntPositive(tk.Entry):
    def __init__(self, master=None, textvariable=None, cnf={}, **kw):
        """
        @param textvariable: tkinter.IntVar object watched by this widget.
               If not given, variable object is generate inside the class.
        """
        tk.Entry.__init__(self, master, cnf, bg='white', **kw)
        if textvariable:
            self.cv = textvariable
        else:
            self.cv = tk.IntVar(master, 1)
        self.config(textvariable=self.cv)

    def set(self, v):
        self.cv.set(v)

    def get(self):
        return self.cv.get()

    def get_nostatechk(self):
        return self.get()

    def validate(self):
        reason = None
        if self.is_disabled():
            return None
        try:
            tmp = self.cv.get()
            if tmp <= 0:
                reason = 'int value is not positive'
        except ValueError:
            reason = 'ValueError (should be Int)'

        if reason:
            self.config(bg='red')
        else:
            self.config(bg='white')
        return reason

    def enable(self):
        self.config(state=tk.NORMAL)

    def disable(self):
        self.config(state=tk.DISABLED)

    def is_disabled(self):
        return self.cget('state') == tk.DISABLED


class DoublePositive(tk.Entry):
    def __init__(self, master=None, textvariable=None, cnf={}, **kw):
        """
        @param textvariable: tkinter.DoubleVar object watched by this widget.
               If not given, variable object is generate inside the class.
        """
        tk.Entry.__init__(self, master, cnf, bg='white', **kw)
        if textvariable:
            self.cv = textvariable
        else:
            self.cv = tk.DoubleVar(master, 1.0)
        self.config(textvariable=self.cv)

    def set(self, v):
        self.cv.set(v)

    def get(self):
        return self.cv.get()

    def get_nostatechk(self):
        return self.get()

    def validate(self):
        reason = None
        if self.is_disabled():
            return reason

        try:
            tmp = self.cv.get()
            if tmp <= 0:
                reason = 'double value is not positive'
        except ValueError:
            reason = 'ValueError (should be Double)'

        if reason:
            self.config(bg='red')
        else:
            self.config(bg='white')

        return reason

    def enable(self):
        self.config(state=tk.NORMAL)

    def disable(self):
        self.config(state=tk.DISABLED)

    def is_disabled(self):
        return self.cget('state') == tk.DISABLED


class DoubleNoNegative(tk.Entry):
    def __init__(self, master=None, textvariable=None, cnf={}, **kw):
        """
        @param textvariable: tkinter.DoubleVar object watched by this widget.
               If not given, variable object is generate inside the class.
        """
        tk.Entry.__init__(self, master, cnf, bg='white', **kw)
        if textvariable:
            self.cv = textvariable
        else:
            self.cv = tk.DoubleVar(master, 0.0)
        self.config(textvariable=self.cv)

    def set(self, v):
        self.cv.set(v)

    def get(self):
        return self.cv.get()

    def get_nostatechk(self):
        return self.get()

    def validate(self):
        reason = None
        if self.is_disabled():
            return reason

        try:
            tmp = self.cv.get()
            if tmp < 0:
                reason = 'double value is negative'
        except ValueError:
            reason = 'ValueError (should be Double)'

        if reason:
            self.config(bg='red')
        else:
            self.config(bg='white')

        return reason

    def enable(self):
        self.config(state=tk.NORMAL)

    def disable(self):
        self.config(state=tk.DISABLED)

    def is_disabled(self):
        return self.cget('state') == tk.DISABLED


def VecNdFactory(Base_, dim_):
    """factory class for N-dim double vector
        @param Base: base widget class such like Int or Double defined above
        @param dim: array dimension
    """
    class V(tk.Frame):
        Base = Base_
        dim = dim_

        def __init__(self, master=None, textvariable=None, *args, **kw):
            """
            @param textvariable:
              [type(Base.cv)]*dim value watched by this widget.
              If not given, variable object is generate inside the class.
            """
            tk.Frame.__init__(self, master)

            self.disabled = False

            if textvariable:
                self.array = [self.Base(self, textvariable=t) for t in textvariable[:self.dim]]
            else:
                self.array = [self.Base(self) for i in range(self.dim)]

            for a in self.array:
                a.pack(side=tk.LEFT)

        def config(self, **kw):
            """apply configuration for all widgets
            """
            for a in self.array:
                a.config(**kw)

        def set(self, val):
            for a, v in zip(self.array, val):
                a.set(v)

        def get(self):
            v = []
            for a in self.array:
                if not a.is_disabled():
                    v.append(a.get())
                else:
                    v.append(None)
            return v

        def get_nostatechk(self):
            v = []
            for a in self.array:
                v.append(a.get_nostatechk())
            return v

        def validate(self):
            if self.is_disabled():
                return None

            err = []
            for idx, widget in enumerate(self.array):
                r = widget.validate()
                if r:
                    err.append(('[{0:d}]'.format(idx), r))

            return err if err else None

        def enable(self):
            for a in self.array:
                a.config(state=tk.NORMAL)
            self.disabled = False

        def disable(self):
            for a in self.array:
                a.config(state=tk.DISABLED)
            self.disabled = True

        def is_disabled(self):
            return self.disabled

    return V

Vec2Int = VecNdFactory(Int, 2)
Vec3Int = VecNdFactory(Int, 3)
Vec3IntPositive = VecNdFactory(IntPositive, 3)
Vec2d = VecNdFactory(Double, 2)
Vec3d = VecNdFactory(Double, 3)
Vec4d = VecNdFactory(Double, 4)
