_PRICE_REGEX = re.compile('\s*([0-9]+|[0-9]+\.[0-9]+)\s+([a-z\-]+)')
_OVERRIDE_REGEX = re.compile('\s*([+\-*/]?)\s*(.+)')
_NUMBER_REGEX = re.compile('[0-9]+(?:\.[0-9]+)?$')
INVALID_OVERRIDE = "Invalid override price \'{}\' for {}"
INVALID_OVERRIDE_RATE = "Invalid override rate \'{}\' = {} for {}. Rate must be a positive number"
INVALID_PRICE = "Invalid price {}"
    CURRENCY_FNAME = "cfg\\currency.json"
    CURRENCY_BASE_FNAME = "res\\currency_base.json"
    CURRENCY_API = [urljoin(POE_NINJA_API, "GetCurrencyOverview?league={}"),
                    urljoin(POE_NINJA_API, "GetFragmentOverview?league={}")]
    UPDATE_INTERVAL = 10  # minutes
    DEFAULT_CONFIDENCE_LEVEL = ConfidenceLevel.Medium.value
    CHAOS_NAME = 'Chaos Orb'
class CurrencyManager:
class CompiledCurrencyManager:
class CurrencyInfo:
class CurrencyEncoder(NoIndentEncoder):
    def __init__(self):
    def init(self):
    def load_base(self):
    def load(self):
    def save(self):
    def compile(self, shorts=None, rates=None, overrides=None, last_update=None):
    def needUpdate(self):
    def update(self, force_update=False, accept_empty=False):
            def get_count(currency):
    def convert(self, amount, short):
    def isPriceValid(self, price_str):
    def toDisplayPrice(self, rate):
    def toWhisper(self, short):
    def toFull(self, short):
    def _apply_override(self, key, val, overrides, path=None):
    def _compile(self, rates, shorts, overrides):
    def priceFromString(price):
    def overridePriceFromString(override_price):
    def isOverridePriceValid(self, override_price):
    def isPriceRelative(override_price):
    def compilePrice(self, override_price, base_price=None):
    def clearCache(cls):
    def fromCCM(self, ccm):
    def toCCM(self):
    def __init__(self, crates, cshorts):
    def __init__(self):
    def default(self, o): FILTER_FILE_MISSING = "Missing file: {}"
FILTER_INVALID_JSON = "Error decoding JSON: {} in file {}"
FILTERS_FILE_VALIDATION_ERROR = "Error validating filters file: {}"
FILTERS_FILE_SCHEMA_ERROR = "Filters file schema is invalid: {}"
_AUTO_FILTERS_FNAME = "tmp\\filters.auto.json"
_USER_FILTERS_FNAME = "cfg\\filters.json"
_USER_DEFAULT_FILTERS_FNAME = "cfg\\filters.example.json"
FILTERS_CFG_FNAME = "cfg\\filters.config.json"
FILTERS_FILE_SCHEMA_FNAME = "res\\filters_file.schema.json"
_URLS = [
    urljoin(POE_NINJA_API, "GetDivinationCardsOverview?league={}"),
    urljoin(POE_NINJA_API, "GetEssenceOverview?league={}"),
    urljoin(POE_NINJA_API, "GetUniqueJewelOverview?league={}"),
    urljoin(POE_NINJA_API, "GetUniqueMapOverview?league={}"),
    urljoin(POE_NINJA_API, "GetUniqueFlaskOverview?league={}"),
    urljoin(POE_NINJA_API, "GetUniqueWeaponOverview?league={}"),
    urljoin(POE_NINJA_API, "GetUniqueArmourOverview?league={}"),
    urljoin(POE_NINJA_API, "GetUniqueAccessoryOverview?league={}"),
    urljoin(POE_NINJA_API, "GetProphecyOverview?league={}"),
    # urljoin(POE_NINJA_API, "GetMapOverview?league={}"
]
_VARIANTS = {
    # Doryani's Invitation
    "Physical":			("([0-9]+)% increased Physical Damage$", ),
    "Cold":				("([0-9]+)% increased Cold Damage$", ),
    "Fire":				("([0-9]+)% increased Fire Damage$", ),
    "Lightning":		("([0-9]+)% increased Lightning Damage$", ),

    # Atziri's Splendour
    "Armour": ("([0-9]+)% increased Armour$", "\\+([0-9]+) to maximum Life$"),
    "Evasion": ("([0-9]+)% increased Evasion Rating$", "\\+([0-9]+) to maximum Life$"),
    "ES": ("([0-9]+)% increased Energy Shield$", "\\+([0-9]+) to maximum Energy Shield$"),

    "Armour/ES": ("([0-9]+)% increased Armour and Energy Shield$", "\\+([0-9]+) to maximum Energy Shield$"),
    "Evasion/ES": ("([0-9]+)% increased Evasion and Energy Shield$", "\\+([0-9]+) to maximum Energy Shield$"),

    "Armour/Evasion": ("([0-9]+)% increased Armour and Evasion$", "\\+([0-9]+) to maximum Life$"),
    "Evasion/ES/Life": ("([0-9]+)% increased Evasion and Energy Shield$", "\\+([0-9]+) to maximum Life$"),
    "Armour/ES/Life": ("([0-9]+)% increased Armour and Energy Shield$", "\\+([0-9]+) to maximum Life$"),

    "Armour/Evasion/ES": ("([0-9]+)% increased Armour, Evasion and Energy Shield$", ),

    # Vessel of Vinktar
    "Added Attacks":	("Adds ([0-9]+) to ([0-9]+) Lightning Damage to Attacks during Flask effect$", ),
    "Added Spells":		("Adds ([0-9]+) to ([0-9]+) Lightning Damage to Spells during Flask effect$", ),
    "Penetration":		("Damage Penetrates ([0-9]+)% Lightning Resistance during Flask effect$", ),
    "Conversion": 		("([0-9]+)% of Physical Damage Converted to Lightning during Flask effect$", ),

    # Beachhead
    "T5": (),
    "T10": (),
    "T15": (),
}
    V1 = 1
    V2 = 2
    UPDATE_INTERVAL = 10  # minutes
    DEFAULT_BUDGET = '200 chaos'
    DEFAULT_MIN_PRICE = ''
    DEFAULT_PRICE_THRESHOLD = '10 chaos'
    DEFAULT_PRICE_OVERRIDE = '* 1'
    DEFAULT_FPRICE_OVERRIDE = '-15 chaos'
    DEFAULT_CONFIDENCE_LEVEL = ConfidenceLevel.Medium.value
    DEFAULT_ENABLE_5L_FILTERS = True
class FilterVersion(IntEnum):
class FilterManager:
class FiltersInfo:
    def __init__(self):
    def init(self):
    def loadConfig(self):
    def saveConfig(self):
    def needUpdate(self):
    def filter_prices(self):
    def fetchFromAPI(self, force_update=False, accept_empty=False):
            def name_to_id(name):
            def get_unique_id(title, name, category, links):
    def _loadDefaultFilters(self, validate=True):
    def loadUserFilters(self, validate=True):
    def loadAutoFilters(self, validate=True):
    def saveUserFilters(self):
    def saveAutoFilters(self):
    def validateFilters(self):
    def updateConfig(self, default_price_override, default_fprice_override, price_threshold, budget, min_price,
                     price_overrides, filter_price_overrides, filter_state_overrides, confidence_level, enable_5l_filters):
    def compileFilters(self, force_validation=False):
    def _compileFilters(self):
    def applyItemPriceOverrides(self, filters):
    def applyOverrides(self, filters):
    def validateConfig(self):
    def validateOverrides(self):
    def getRawFilters(self):
    def getFilterIds(self):
    def getFilters(self):
    def getActiveFilters(self):
    def compileFilter(self, fltr, path=None):
    def getCategories(self):
    def getPrices(filters):
    def getCompiledPrices(filters):
    def getFilterById(id, filters, match=lambda x, y: x.id == y):
    def getFiltersById(id, filters, match=lambda x, y: x.id == y):
    def saveFiltersToFile(cls, fname, filters):
    def loadFiltersFromFile(cls, fname, validate_data):
    def clearCache(cls):
    def convert(ver, filters):
            def is_type_rarity(item_type):
            def type_to_item_class(item_type):
def lower_json(x):
    def __init__(self): class ItemCollection:
    BASE_TYPES_FNAME = 'res\\BaseItemTypes.json'

    base_types = {}
    base_type_to_id = {}
    SIX_LINK_EXCEPTIONS = ('The Goddess Unleashed Eternal Sword', 'Tabula Rasa Simple Robe', 'Skin of the Loyal Simple Robe', 'Skin of the Lords Simple Robe')

    @classmethod
    def init(cls):
        try:
            with open(cls.BASE_TYPES_FNAME) as f:
                data = json.load(f)

            cls.base_types = data
            cls.base_type_to_id = {base_type: class_id for class_id in data for base_type in data[class_id]}
        except Exception as e:
            raise AppException('Failed loading item base types.\n{}\n'
                               'Make sure the file are valid and in place.'.format(e))

    @classmethod
    def get_base_types_by_class(cls, item_class):
        classes = {iclass.name for iclass in ItemClass if item_class & iclass == iclass}
        bases = [cls.base_types.get(iclass, []) for iclass in classes]
        return list(chain.from_iterable(bases)) class Filter:
    FILTER_SCHEMA_FNAME = "res\\filter.schema.json"
    FILTER_INVALID_PRICE = "Invalid price '{}' in filter {}"
    FILTER_INVALID_PRICE_BASE = "Invalid price in filter: {}. Expected filter to have a base"
    FILTER_INVALID_REGEX = "Invalid regex: '{}' in filter {}. Error while compiling: {}"

    _FILTER_PRICE_REGEX = re.compile('\s*([+\-*/]?)\s*(.+)')
    _NUMBER_REGEX = re.compile('[0-9]+(?:\.[0-9]+)?$')

    schema_validator = None

    def __init__(self, title, criteria=None, enabled=True, category='', id='', base_id='', desc='',
                 priority=FilterPriority.UserBase.value):
        self.title = title
        self.criteria = criteria if criteria is not None else {}
        self.enabled = enabled
        self.category = category
        self.priority = priority
        self.description = desc

        self.id = id
        self.baseId = base_id

    @classmethod
    def init(cls):
        try:
            with open(Filter.FILTER_SCHEMA_FNAME) as f:
                schema = json.load(f)

            jsonschema.validate({}, schema)
        except jsonschema.ValidationError:
            cls.schema_validator = jsonschema.Draft4Validator(schema)
        except jsonschema.SchemaError as e:
            raise AppException('Failed loading filter validation schema.\n{}'.format(FILTER_SCHEMA_ERROR.format(e)))
        # except FileNotFoundError as e:
        except Exception as e:
            raise AppException('Failed loading filter validation schema.\n{}\n'
                               'Make sure the file are valid and in place.'.format(e))

    def validate(self):
        try:
            data = self.toDict()
            self.schema_validator.validate(data)
        except jsonschema.ValidationError as e:
            raise AppException(FILTER_VALIDATION_ERROR.format(get_verror_msg(e, data)))

        if self.criteria:
            for price in ('price_min', 'price_max'):
                if not self.baseId and price in self.criteria:
                    if not cm.isOverridePriceValid(self.criteria[price]):
                        raise AppException(Filter.FILTER_INVALID_PRICE.format(self.criteria[price], self.title))

                    if cm.isPriceRelative(self.criteria[price]):
                        raise AppException(Filter.FILTER_INVALID_PRICE_BASE.format(self.criteria[price]))

            try:
                fgs = [FilterGroupFactory.create(FilterGroupType(fg['type']), fg) for fg in self.criteria.get('fgs', [])]
                for fg in fgs:
                    for mf in fg.mfs:
                        if mf.type != ModFilterType.Pseudo:
                            re.compile(mf.expr)

            except re.error as e:
                raise AppException(Filter.FILTER_INVALID_REGEX.format(e.pattern, self.title, e))

            #TODO rairty/iclass validate?

    def compile(self, base={}):
        crit = self.criteria

        comp = dict(base)

        for key in crit:
            # if key == 'type':
            #     types = []
            #     for itype in crit['type']:
            #         for id in _ITEM_TYPE:
            #             if itype == _ITEM_TYPE[id]:
            #                 types.append(id)
            #                 break
            #     comp['type'] = types
            if key == 'rarity':
                comp[key] = [_NAME_TO_TYPE[itype] for itype in crit[key]]
            elif key == 'iclass':
                comp[key] = ItemClass[crit[key]]
            elif key == 'name':
                comp['name'] = [name.lower() for name in crit[key]]

            elif key in ('price_min', 'price_max'):
                comp[key] = cm.compilePrice(crit[key], comp.get(key, None))

            elif key == 'fgs':
                fgs = [FilterGroupFactory.create(FilterGroupType(fg['type']), fg) for fg in crit[key]]

                for fg in fgs:
                    for mf in fg.mfs:
                        if mf.type != ModFilterType.Pseudo:
                            mf.expr = re.compile(mf.expr)
                comp[key] = fgs
            else:
                comp[key] = crit[key]

        return comp

    @property
    def isChild(self):
        return self.baseId and self.baseId.lower() != self.id.lower()

    def isChildOf(self, fltr):
        return self.isChild and self.baseId.lower() == fltr.id.lower()

    def toDict(self):
        return {
                'title': self.title,
                'enabled': self.enabled,
                'category': self.category,
                'id': self.id,
                'baseid': self.baseId,
                'priority': self.priority,
                'description': self.description,
                'criteria': self.criteria}

    @classmethod
    def fromDict(cls, data):
        return cls(
            data.get('title', data.get('id', '')),
            data.get('criteria', {}),
            data.get('enabled', True),
            data.get('category', 'user'),
            data.get('id', ''),
            data.get('baseid', ''),
            data.get('description', ''),
            data.get('priority', FilterPriority.UserBase.value)) class FilterPriority(IntEnum):
    Min = -10
    AutoBase = 0
    UserBase = 10
    Max = 50 class ModFilter:
    __slots__ = ['type', 'expr', 'min', 'max']

    def __init__(self, mod_type=ModFilterType.Total, expr='', min_val=None, max_val=None):
        self.type = mod_type
        self.expr = expr
        self.min = float(min_val) if min_val is not None else min_val
        self.max = float(max_val) if max_val is not None else max_val

    @classmethod
    def fromData(cls, data):
        return cls(ModFilterType(data['type']), data['expr'], data.get('min', None), data.get('max', None))

    def toDict(self):
        return {
            'type': self.type.value,
            'expr': self.expr,
            'min': self.min,
            'max': self.max
        }

    def __deepcopy__(self, memo):
        cls = self.__class__
        result = cls.__new__(cls)
        memo[id(self)] = result

        for k in self.__slots__:
            v = getattr(self, k)
            if k == 'expr' and isinstance(v, RE_COMPILED_TYPE):
                setattr(result, k, re.compile(v.pattern))
            else:
                setattr(result, k, copy.deepcopy(v))
        return result class ModFilterType(Enum):
    Total = 'total'
    Implicit = 'implicit'
    Explicit = 'explicit'
    Crafted = 'crafted'
    Enchant = 'enchant'
    Prophecy = 'prophecy'
    Leaguestone = 'leaguestone'
    Pseudo = 'pseudo'

    # @classmethod
    # def fromName(cls, name):
    #     return _nameToType[name] class FilterGroupFactory:
    @staticmethod
    def create(fg_type, data):
        if fg_type == FilterGroupType.All:
            fg = AllFilterGroup()
        elif fg_type == FilterGroupType.Count:
            fg = CountFilterGroup()
        elif fg_type == FilterGroupType.Nothing:
            fg = NoneFilterGroup()
        else:
            raise ValueError('Invalid filter group type: {}'.format(fg_type))

        fg.fromDict(data)
        return fg class FilterGroupType(Enum):
    All = "all"
    Count = "count"
    Nothing = "none" class CountFilterGroup(ModFilterGroup):
    __slots__ = ('match_min', 'match_max')
    def __init__(self, mod_filters=None, match_min=None, match_max=None):
        super().__init__(mod_filters)
        self.match_min = match_min
        self.match_max = match_max

    def fromDict(self, data):
        super().fromDict(data)
        self.match_min = data.get('match_min', None)
        self.match_max = data.get('match_max', None)

    def toDict(self):
        data = super().toDict()
        data['match_min'] = self.match_min
        data['match_max'] = self.match_max
        return data

    def checkMods(self, item):
        match_min = len(self.mfs) if self.match_min is None else self.match_min
        match_max = len(self.mfs) if self.match_max is None else self.match_max
        return checkmods_count(item, self.mfs, match_min, match_max)

    def getType(self):
        return FilterGroupType.Count class ModsHelper:
    MODS_FNAME = 'res\\mods.json'
    MOD_TEXT_REGEX = re.compile('\(([^()]+)\)\s+(.*)')
    def __init__(self):
    def init(self):
    def load(self):
    def modToText(self, mod_type, expr):
    def isCustom(self, mod_type, expr):
    def isPredefined(self, mod_text):
    def textToMod(self, mod_text):
    def _getTagText(self, text):
    def stripTags(self, mod_text):
    def modToParam(self, mod_type, expr): class AppException(Exception):
    pass class MultiCombobox(Menubutton):
    def __init__(self, master, kw_vals, default_text='Any', **kwargs):
        super().__init__(master, **kwargs)

        self.default_text = default_text
        self.kw_vals = kw_vals
        self.kw_vals_inv = dict(map(reversed, kw_vals.items()))

        ui_style = Style()
        ui_style.configure('MultiCombobox.TMenubutton', relief=RAISED, padding=3, anchor=CENTER)
        ui_style.layout('MultiCombobox.TMenubutton', _layout)
        self.config(style='MultiCombobox.TMenubutton')

        menu = Menu(self, tearoff=False,
                    activeforeground='SystemHighlightText', activebackground='SystemHighlight',
                    foreground='SystemWindowText', background='SystemWindow',
                    disabledforeground='SystemGrayText', bd=0, activeborderwidth=1)
        self.configure(menu=menu)

        self.any_var = BooleanVar(value=True)
        menu.add_checkbutton(label=default_text, variable=self.any_var,
                             onvalue=True, offvalue=False,
                             command=self.anySelected)

        self.choices = {}
        for i, choice in enumerate(kw_vals):
            self.choices[choice] = BooleanVar()
            # columnbreak = (i+1) % 4 == 0
            columnbreak = False
            menu.add_checkbutton(label=choice, variable=self.choices[choice],
                                 onvalue=True, offvalue=False, columnbreak=columnbreak,
                                 command=self.updateValue)
        self.updateValue()

    def updateValue(self):
        selected = [choice for choice in self.kw_vals if self.choices[choice].get()]

        if selected:
            self.any_var.set(False)
            text = self._list_to_field(selected)
        else:
            self.any_var.set(True)
            text = self.default_text

        self.config(text=text)

    def anySelected(self):
        if self.any_var.get():
            self.set_value()
        else:
            self.any_var.set(True)

    def _list_to_field(self, lst):
        s = None
        for item in lst:
            if not s:
                s = str(item)
            else:
                s += ', ' + str(item)
        return s

    def get_value(self):
        selected = [self.kw_vals[choice] for choice in self.kw_vals if self.choices[choice].get()]
        if selected:
            return selected
        return None

    def set_value(self, vals=None):

        if vals is None:
            vals = []

        for choice in self.choices:
            self.choices[choice].set(False)

        for val in vals:
            try:
                self.choices[self.kw_vals_inv[val]].set(True)
            except KeyError:
                pass

        self.updateValue() class MyTreeview(Treeview):
    def __init__(self, master, **kw):
        super().__init__(master, **kw)

    def tag_add(self, tagname, items):
        self.tk.call(self._w, 'tag', 'add', tagname, items)

    def tag_remove(self, tagname, items):
        self.tk.call(self._w, 'tag', 'remove', tagname, items)

    def tag_names(self):
        return self.tk.splitlist(self.tk.call(self._w, 'tag', 'names'))

    def search(self, search_text, find_next=False):
        if not search_text:
            return

        selected = self.selection()
        if not selected:
            children = self.get_children()
            if not children:
                return  # empty tree
            start_iid = children[0]
        else:
            start_iid = selected[0]

        search_text = re.escape(search_text)

        if find_next:
            start_iid = self._get_next(start_iid)

        # print('start ID: ', start_iid)
        curr_iid = start_iid

        while True:
            text = self.item(curr_iid, 'text')
            # print(text)
            if re.search(search_text, text, re.IGNORECASE):
                self.selection_set(curr_iid)
                self.see(curr_iid)
                break

            curr_iid = self._get_next(curr_iid)

            if curr_iid == start_iid:
                break

    def _get_next(self, curr_iid):
        children = self.get_children(curr_iid)
        if children:
            next_iid = children[0]
        else:
            while True:
                next_iid = self.next(curr_iid)
                if not next_iid:  # last child -> get the next of parent
                    curr_iid = self.parent(curr_iid)
                    if not curr_iid:  # last item in tree -> get the first item
                        # reached_end = True
                        next_iid = self.get_children()[0]
                if next_iid:
                    break

        return next_iid

    def clear(self):
        self.delete(*self.get_children()) class Scrolling_Area(Frame, object):
    def __init__(self, master, width=None, anchor=N, height=None, mousewheel_speed=2, scroll_horizontally=True,
                 xscrollbar=None, scroll_vertically=True, yscrollbar=None, background=None, inner_frame=Frame, **kw):
        Frame.__init__(self, master, class_="Scrolling_Area", background=background)

        self.grid_columnconfigure(0, weight=1)
        self.grid_rowconfigure(0, weight=1)

        self._width = width
        self._height = height

        self.canvas = Canvas(self, background=background, highlightthickness=0, width=width, height=height)
        self.canvas.grid(row=0, column=0, sticky=N + E + W + S)

        if scroll_vertically:
            if yscrollbar is not None:
                self.yscrollbar = yscrollbar
            else:
                self.yscrollbar = AutoScrollbar(self, orient=VERTICAL)
                self.yscrollbar.grid(row=0, column=1, sticky=N + S)

            self.canvas.configure(yscrollcommand=self.yscrollbar.set)
            self.yscrollbar['command'] = self.canvas.yview
        else:
            self.yscrollbar = None

        if scroll_horizontally:
            if xscrollbar is not None:
                self.xscrollbar = xscrollbar
            else:
                self.xscrollbar = AutoScrollbar(self, orient=HORIZONTAL)
                self.xscrollbar.grid(row=1, column=0, sticky=E + W)

            self.canvas.configure(xscrollcommand=self.xscrollbar.set)
            self.xscrollbar['command'] = self.canvas.xview
        else:
            self.xscrollbar = None

        self.rowconfigure(0, weight=1)
        self.columnconfigure(0, weight=1)

        self.innerframe = inner_frame(self.canvas, **kw)
        self.innerframe.grid(row=0, column=0, sticky='nsew')
        # self.innerframe.pack(anchor=anchor)

        self.canvas.create_window(0, 0, window=self.innerframe, anchor='nw', tags="inner_frame")

        self.canvas.bind('<Configure>', self._on_canvas_configure)
        self.innerframe.bind('<Configure>', self._on_frame_configure)

        # Mousewheel_Support(self).add_support_to(self.canvas, xscrollbar=self.xscrollbar, yscrollbar=self.yscrollbar)

    @property
    def width(self):
        return self.canvas.winfo_width()

    @width.setter
    def width(self, width):
        self.canvas.configure(width=width)

    @property
    def height(self):
        return self.canvas.winfo_height()

    @height.setter
    def height(self, height):
        self.canvas.configure(height=height)

    def set_size(self, width, height):
        self.canvas.configure(width=width, height=height)

    def _on_canvas_configure(self, event):
        # width = max(self.innerframe.winfo_reqwidth(), event.width)
        # height = max(self.innerframe.winfo_reqheight(), event.height)
        width = max(self.innerframe.winfo_reqwidth(), event.width)
        height = self.innerframe.winfo_reqheight()

        self.canvas.configure(scrollregion="0 0 %s %s" % (width, height))
        self.canvas.itemconfigure("inner_frame", width=width)

    def _on_frame_configure(self, event):
        # width = max(self.innerframe.winfo_reqwidth(), self.canvas.winfo_width())
        # height = max(self.innerframe.winfo_reqheight(), self.canvas.winfo_height())

        self.canvas.configure(scrollregion="0 0 %s %s" % (event.width, event.height))

    def update_viewport(self):
        self.update()

        window_width = self.innerframe.winfo_reqwidth()
        window_height = self.innerframe.winfo_reqheight()

        if self._width is None:
            canvas_width = window_width
        else:
            canvas_width = min(self._width, window_width)

        if self._height is None:
            canvas_height = window_height
        else:
            canvas_height = min(self._height, window_height)

        self.canvas.configure(scrollregion="0 0 %s %s" % (window_width, window_height), width=canvas_width,
                              height=canvas_height)
        self.canvas.itemconfigure("inner_frame", width=window_width, height=window_height) class TooltipEntry(Entry):
    def __init__(self, parent, *args, **kw):
        Entry.__init__(self, parent, *args, **kw)

        self.tooltip = Tooltip2(self, waittime=0, text='', bg='white')  #, pad=(0, 0, 0, 0))

    def showTooltip(self, text=None, pos=None):
        self.tooltip.show(text, pos)

    def hideTooltip(self):
        self.tooltip.hide()

    def destroy(self):
        self.tooltip.hide()
        super().destroy() TIP_BELOW = 'below' TIP_ABOVE = 'above' class Combobox_Autocomplete(TooltipEntry, object):

    def __init__(self, master, list_of_items=None, autocomplete_function=None, listbox_width=None, listbox_height=7,
                 ignorecase_match=True, startswith_match=True, vscrollbar=True, hscrollbar=True, *args, **kwargs):
        if hasattr(self, "autocomplete_function"):
            if autocomplete_function is not None:
                raise ValueError("Combobox_Autocomplete subclass has 'autocomplete_function' implemented")
        else:
            if autocomplete_function is not None:
                self.autocomplete_function = autocomplete_function
            else:
                if list_of_items is None:
                    raise ValueError("If not given complete function, list_of_items can't be 'None'")

                if not ignorecase_match:
                    if startswith_match:
                        def matches_function(entry_data, item):
                            return item.startswith(entry_data)
                    else:
                        def matches_function(entry_data, item):
                            return item in entry_data

                    self.autocomplete_function = lambda entry_data: [item for item in self.list_of_items if
                                                                     matches_function(entry_data, item)]
                else:
                    if startswith_match:
                        def matches_function(escaped_entry_data, item):
                            if re.match(escaped_entry_data, item, re.IGNORECASE):
                                return True
                            else:
                                return False
                    else:
                        def matches_function(escaped_entry_data, item):
                            if re.search(escaped_entry_data, item, re.IGNORECASE):
                                return True
                            else:
                                return False

                    def autocomplete_function(entry_data):
                        escaped_entry_data = re.escape(entry_data)
                        return [item for item in self.list_of_items if matches_function(escaped_entry_data, item)]

                    self.autocomplete_function = autocomplete_function

        self._listbox_height = int(listbox_height)
        self._listbox_width = listbox_width

        self.list_of_items = list_of_items

        self._use_vscrollbar = vscrollbar
        self._use_hscrollbar = hscrollbar

        # kwargs.setdefault("background", "white")

        if "textvariable" in kwargs:
            self._entry_var = kwargs["textvariable"]
        else:
            self._entry_var = kwargs["textvariable"] = StringVar()

        # TooltipEntry.__init__(self, master, **kwargs)
        super().__init__(master, *args, **kwargs)

        self._trace_id = self._entry_var.trace('w', self._on_change_entry_var)

        self._listbox = None

        self.bind("<Control-space>",lambda event: self.post_listbox())
        self.bind("<Up>", self._previous)
        self.bind("<Down>", self._next)
        self.bind('<Control-n>', self._next)
        self.bind('<Control-p>', self._previous)

        self.bind("<Return>", self._update_entry_from_listbox)
        self.bind("<Escape>", lambda event: self.unpost_listbox())
        # self.bind("<FocusOut>", lambda event: self._update_entry_from_listbox(event, False))
        # self.bind("<FocusOut>", lambda event: self.after(50, self._on_focus_out, event))
        self.bind("<FocusOut>", self._on_focus_out)
        parent = self.master
        while not isinstance(parent, (Toplevel, Tk)):
            parent = parent.master

        self.parent_window = parent

    def _on_focus_out(self, event):
        if self._listbox and self._listbox.winfo_name() in str(self._listbox.focus_get()):
            return
        self.unpost_listbox()
        self.event_generate('<<TrueFocusOut>>')

    def _on_change_entry_var(self, name, index, mode):

        entry_data = self._entry_var.get()

        if entry_data == '':
            self.unpost_listbox()
            # self.focus()
        else:
            values = self.autocomplete_function(entry_data)
            if values:
                if self._listbox is None:
                    self._build_listbox(values)
                else:
                    self._listbox.delete(0, END)

                    height = min(self._listbox_height, len(values))
                    self._listbox.configure(height=height)

                    for item in values:
                        self._listbox.insert(END, item)

            else:
                self.unpost_listbox()
                self.focus()

    def _build_listbox(self, values):
        listbox_frame = Frame(self.parent_window, padding=0)

        if self._listbox_width:
            width = self._listbox_width
        else:
            width = self.winfo_width()

        self._listbox = Listbox(listbox_frame, background="white", selectmode=SINGLE, activestyle="none",
                                exportselection=False)
        self._listbox.grid(row=0, column=0, sticky='nsew', padx=0, pady=0)

        self._listbox.bind("<ButtonRelease-1>", self._update_entry_from_listbox)
        self._listbox.bind("<Return>", self._update_entry_from_listbox)
        self._listbox.bind("<Escape>", lambda event: self.unpost_listbox())
        # self._listbox.bind("<FocusOut>", lambda event: self.after(50, self._on_focus_out_list, event))
        self._listbox.bind('<Control-n>', self._next)
        self._listbox.bind('<Control-p>', self._previous)

        if self._use_vscrollbar:
            vbar = Scrollbar(listbox_frame, orient=VERTICAL, command=self._listbox.yview)
            vbar.grid(row=0, column=1, sticky='ns')

            self._listbox.configure(yscrollcommand=lambda f, l: autoscroll(vbar, f, l))

        if self._use_hscrollbar:
            hbar = Scrollbar(listbox_frame, orient=HORIZONTAL, command=self._listbox.xview)
            hbar.grid(row=1, column=0, sticky='ew')

            self._listbox.configure(xscrollcommand=lambda f, l: autoscroll(hbar, f, l))

        listbox_frame.grid_columnconfigure(0, weight=1)
        listbox_frame.grid_rowconfigure(0, weight=1)

        listbox_frame.place(in_=self, x=0, y=self.winfo_height(), width=width)

        height = min(self._listbox_height, len(values))
        self._listbox.configure(height=height)

        for item in values:
            self._listbox.insert(END, item)

    def post_listbox(self):
        if self._listbox is not None: return

        entry_data = self._entry_var.get()
        if entry_data == '': return

        values = self.autocomplete_function(entry_data)
        if values:
            self._build_listbox(values)

    def unpost_listbox(self):
        if self._listbox is not None:
            self._listbox.master.destroy()
            self._listbox = None

    def get_value(self):
        return self._entry_var.get()

    def set_value(self, text, close_dialog=False):
        self._set_var(text)

        if close_dialog:
            self.unpost_listbox()

        self.icursor(END)
        self.xview_moveto(1.0)

    def _set_var(self, text):
        self._entry_var.trace_vdelete("w", self._trace_id)
        self._entry_var.set(text)
        self._trace_id = self._entry_var.trace('w', self._on_change_entry_var)

    def _update_entry_from_listbox(self, event, focus=True):
        if self._listbox is not None:
            current_selection = self._listbox.curselection()

            if current_selection:
                text = self._listbox.get(current_selection)
                self._set_var(text)

            self._listbox.master.destroy()
            self._listbox = None

            if focus:
                self.focus()
                self.icursor(END)
                self.xview_moveto(1.0)

        return "break"

    def _previous(self, event):
        if self._listbox is not None:
            current_selection = self._listbox.curselection()

            if len(current_selection) == 0:
                self._listbox.selection_set(0)
                self._listbox.activate(0)
            else:
                index = int(current_selection[0])
                self._listbox.selection_clear(index)

                if index == 0:
                    index = END
                else:
                    index -= 1

                self._listbox.see(index)
                self._listbox.selection_set(first=index)
                self._listbox.activate(index)

        return "break"

    def _next(self, event):
        if self._listbox is not None:

            current_selection = self._listbox.curselection()
            if len(current_selection) == 0:
                self._listbox.selection_set(0)
                self._listbox.activate(0)
            else:
                index = int(current_selection[0])
                self._listbox.selection_clear(index)

                if index == self._listbox.size() - 1:
                    index = 0
                else:
                    index += 1

                self._listbox.see(index)
                self._listbox.selection_set(index)
                self._listbox.activate(index)
        return "break" class PlaceholderEntry(TooltipEntry):
    def __init__(self, parent, ph_text, ph_style=None, *args, **kw):
        # if 'textvariable' in kw:
        #     self._entry_var = kw['textvariable']
        # else:
        #     self._entry_var = kw['textvariable'] = StringVar()

        super().__init__(parent, *args, **kw)

        if ph_style is None:
            ph_style = self.cget('style')
            if not ph_style:
                ph_style = self.winfo_class()
            ph_style = 'Placeholder.' + ph_style

        self.normal_style = self.cget('style')
        self.ph_style = ph_style
        self.ph_text = ph_text
        self.with_placeholder = False

        self.bind('<FocusIn>', self.onFocusIn, add="+")
        self.bind('<FocusOut>', self.onFocusOut, add="+")
        self.onFocusOut()

    def Reset(self):
        self.delete(0, "end")
        self.onFocusOut()

    def onFocusIn(self, event=None):
        if self.with_placeholder:
            self.delete(0, "end")
            self.config(style=self.normal_style)
            self.with_placeholder = False

    def onFocusOut(self, event=None):
        if self.get() == '':
            self.insert(0, self.ph_text)
            self.config(style=self.ph_style)
            self.with_placeholder = True

    def get_value(self):
        if self.with_placeholder:
            return ''
        return self.get()

    def set_value(self, val):
        self.onFocusIn()
        self.insert(END, val)
        self.onFocusOut()

    def set_state(self, state):
        if state == DISABLED:
            self.onFocusIn()
        else:
            self.onFocusOut()
        self.config(state=state)

    def insert_value(self, index, string):
        self.onFocusIn()
        self.insert(index, string)
        self.onFocusOut() class Spinbox(ttk.Entry):

    def __init__(self, master=None, **kw):
        ttk.Entry.__init__(self, master, "ttk::spinbox", **kw)

    def current(self, newindex=None):
        return self.tk.call(self._w, 'current', newindex)

    def set(self, value):
        return self.tk.call(self._w, 'set', value)
import collections
import copy
import functools
import threading
import tkinter.font as tkfont
import itertools
from enum import Enum
from tkinter import *
from tkinter import messagebox
from tkinter.ttk import *
from lib.CurrencyManager import cm
from lib.FilterManager import FilterManager, fm, lower_json
from lib.ItemClass import ItemClass, id_to_name
from lib.ItemCollection import ItemCollection
from lib.ItemFilter import Filter, FilterPriority
from lib.ModFilter import ModFilter, ModFilterType
from lib.ModFilterGroup import FilterGroupFactory, FilterGroupType, CountFilterGroup
from lib.ModsHelper import mod_helper
from lib.Utility import AppException
from ui.MultiCombobox import MultiCombobox
from ui.MyTreeview import MyTreeview
from ui.ScrollingFrame import Scrolling_Area
from ui.TooltipEntry import TooltipEntry, TIP_BELOW, TIP_ABOVE
from ui.cmb_autocomplete import Combobox_Autocomplete
from ui.entry_placeholder import PlaceholderEntry
from ui.ttk_spinbox import Spinbox

        # elif self.currFilter and self.currFilter.id.lower() != val and val in self.lst_ids:
        #     return False, 'Filter id must be unique'

        return True, ''

    def _validate_id_field(self):
        valid, reason = self._is_id_field_valid()

        if valid:
            self.entry_id.hideTooltip()
            return True
        else:
            self.entry_id.showTooltip(reason)
            return False

    def _is_baseid_field_valid(self):
        val = self.entry_baseid.get_value().lower()

        if val == '' or val in self.lst_ids:
            return True, ''
        else:
            return False, 'Base ID not found'

    def _validate_baseid_field(self):
        valid, reason = self._is_baseid_field_valid()

        if valid:
            self.entry_baseid.hideTooltip()
            return True
        else:
            self.entry_baseid.showTooltip(reason)
            return False

    def _is_title_field_valid(self):
        val = self.entry_title.get_value()

        if self.currFilter and self.currFilter not in self.fm.autoFilters and not val:
            return False, 'Title is a required field'
        return True, ''

    def _validate_title_field(self):
        valid, reason = self._is_title_field_valid()
        if valid:
            self.entry_title.hideTooltip()
            # self.lbl_title_error.config(text='')
            return True
        else:
            self.entry_title.showTooltip(reason)
            return False

    def _is_price_valid(self, val):
        if val.strip() == '':
            return True, ''
        if not cm.isOverridePriceValid(val):
            return False, 'Price format is: <amount> <currency>\nExamples: 100 chaos, 20 ex, 5 divine\n' \
                          'Prices can also be relative to their base.\n' \
                          'Examples: / 3, * 0.5, +2.5 ex, -20 chaos'
        return True, ''

    def _validate_price_field(self, widget):
        valid, reason = self._is_price_valid(widget.get())
        if valid:
            widget.hideTooltip()
            return True
        else:
            widget.showTooltip(reason, pos=TIP_BELOW)
            return False

    def validateForm(self):
        """ This function provides validation for all fields with tooltips.
        When there are multiple errors, the user will be lead from one error to the next until all are resolved. """

        self.entry_title.hideTooltip()
        self.entry_id.hideTooltip()
        self.entry_baseid.hideTooltip()
        self.entry_price_min.hideTooltip()
        self.entry_price_max.hideTooltip()

        for child in list(self.frm_fgs.children.values()):
            if isinstance(child, FilterGroupWidget):
                for mfw in child.mfws:
                    mfw.entry_mod.hideTooltip()

        if not self._validate_title_field():
            self.entry_title.focus_set()
            return False
        if not self._validate_id_field():
            self.entry_id.focus_set()
            return False
        if not self._validate_baseid_field():
            self.entry_baseid.focus_set()
            return False
        if not self._validate_price_field(self.entry_price_min):
            self.entry_price_min.focus_set()
            return False
        if not self._validate_price_field(self.entry_price_max):
            self.entry_price_max.focus_set()
            return False

        for child in list(self.frm_fgs.children.values()):
            if isinstance(child, FilterGroupWidget):
                for mfw in child.mfws:
                    if not mfw.validateMod():
                        mfw.entry_mod.focus_set()
                        return False

        return True

    def _add_min_max(self, parent, **kw):
        frm = Frame(parent, style='Borderless.TFrame')
        self.addColumn(frm)
        frm.rowconfigure(0, weight=1)
        frm.columnconfigure(0, weight=1)
        frm.columnconfigure(1, weight=1)
        frm.grid_propagate(0)

        entry_min = TooltipEntry(frm, style='Default.TEntry', **kw)
        self.addColumn(entry_min, padx=0, pady=0, sticky='nsew')


        entry_max = TooltipEntry(frm, style='Default.TEntry', **kw)