_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
            if isinstance(fg, CountFilterGroup):
                if fg.match_min:
                    self.entry_min.insert(0, str(fg.match_min))
                if fg.match_max:
                    self.entry_max.insert(0, str(fg.match_max))
            for mf in fg.mfs:
                self.addMFW(mf)

    def removeMFW(self, mfw):
        self.mfws.remove(mfw)

    def toFilterGroup(self):
        fg_type = FilterGroupType[self.cmb_type.get()]
        data = {}
        if self.entry_min.get():
            data['match_min'] = int(self.entry_min.get())
        if self.entry_max.get():
            data['match_max'] = int(self.entry_max.get())

        fg = FilterGroupFactory.create(fg_type, data)
        for mfw in self.mfws:
            mf = mfw.toModFilter()
            if mf.expr:
                fg.mfs.append(mf)

        # if isinstance(fg, CountFilterGroup):
        #     if self.entry_min.get():
        #         fg.match_min = self.entry_min.get()
        #     if self.entry_max.get():
        #         fg.match_max = self.entry_max.get()

        return fg

    def set_state(self, state):
        for mfw in self.mfws:
            mfw.set_state(state)

        if state == NORMAL:
            self.cmb_type.config(state=READONLY)
        else:
            self.cmb_type.config(state=DISABLED)

        self.entry_min.config(state=state)
        self.entry_max.config(state=state)
        self.btn_remove.config(state=state)

        self.btn_add_mod.config(state=state)
        self.btn_add_custom.config(state=state)

class FilterEditor(Toplevel):

    _INIT_ERROR = 0
    _INIT_SUCCESS = 1

    def __init__(self, parent):
        Toplevel.__init__(self, parent)

        self.withdraw()
        self.protocol('WM_DELETE_WINDOW', self.onClose)
        self.title('Filter Editor')
        # self.iconbitmap('res\\app.ico')

        # ui_style = Style()
        # # ui_style.configure('Default.TEntry', background='white', highlightbackground="#bebebe", highlightthickness=1, bd=1)
        # ui_style.configure('Default.TEntry', padding=1)
        # ui_style.configure('Placeholder.Default.TEntry', foreground='grey')
        # ui_style.configure('Autocomplete.TEntry') #, borderwidth=0, highlightthickness=1)

        self.positions = {}
        self.filter_form = FormHelper()
        self.fm = fm
        self.currFilter = None
        self.selected = None
        self.objToTreeId = {}  # Obj to tree id
        self.treeIdToObj = {}  # Tree id to Obj
        self.compiling = BooleanVar(False)
        self.compiling.trace('w', lambda a, b, c: self._onCompilingChange())
        self.reload_needed = False

        self.lst_categories = []
        self.lst_ids = []
        # self.lst_base_types = []
        self.lst_base_types = sorted(ItemCollection.base_type_to_id.keys())

        self.configure(padx=10, pady=10)
        self.rowconfigure(0, weight=1)
        self.columnconfigure(1, minsize=250)
        self.columnconfigure(0, weight=1)
        self.columnconfigure(3, weight=1)

        self.frm_tree = Frame(self, style='Borderless.TFrame')
        self.frm_tree.grid(row=0, column=1, sticky='nsew', padx=(0, 5))
        self.frm_tree.columnconfigure(1, weight=1)
        self.frm_tree.rowconfigure(2, weight=1)

        self.frm_tree_btns_border = Frame(self.frm_tree, relief=SOLID, borderwidth=2)
        self.frm_tree_btns_border.grid(row=0, column=0, columnspan=2, sticky='nsew', ipadx=5, ipady=5)

        self.frm_tree_btns_border.rowconfigure(0, weight=1)
        self.frm_tree_btns_border.columnconfigure(0, weight=1)
        self.frm_tree_btns = Frame(self.frm_tree_btns_border)
        self.frm_tree_btns.grid(row=0, column=0)
        self.frm_tree_btns.columnconfigure(0, weight=1)
        self.frm_tree_btns.columnconfigure(1, weight=1)
        self.frm_tree_btns.columnconfigure(2, weight=1)

        self.btn_new = Button(self.frm_tree_btns, text='New', command=self.addNewFilter)
        self.btn_new.grid(row=0, column=0, sticky='nsew')
        self.btn_delete = Button(self.frm_tree_btns, text='Delete', command=self.deleteFilter)
        self.btn_delete.grid(row=0, column=1, sticky='nsew')
        self.btn_copy = Button(self.frm_tree_btns, text='Copy', command=self.copyFilter)
        self.btn_copy.grid(row=0, column=2, sticky='nsew')

        self.search_var = StringVar()
        self.entry_tree_search = PlaceholderEntry(self.frm_tree, 'Search filter..', style='Default.TEntry', textvariable=self.search_var)
        self.entry_tree_search.grid(row=1, column=0, columnspan=2, pady=5, sticky='nsew')
        # self.entry_tree_search.bind('<KeyRelease>', lambda event: self.filterTreeview(self.entry_tree_search.get_value()))

        self.vscroll_tree = Scrollbar(self.frm_tree)
        self.vscroll_tree.grid(row=2, column=0, sticky='ns')

        self.tree = MyTreeview(self.frm_tree, selectmode=BROWSE, yscrollcommand=self.vscroll_tree.set, show='tree')