import tkinter as tk
from typing import TYPE_CHECKING
import logging
from yasuki_gui.ui.deck_builder.components import ScrollableListBox
from yasuki_gui.ui.deck_builder.card_preview import format_card_display_name
from yasuki_gui.ui.deck_builder.deck_data import card_in_side
from yasuki_core.search import parse_and_build_query
if TYPE_CHECKING:
from yasuki_gui.ui.deck_builder.filter_dialog import FilterOptions
logger = logging.getLogger(__name__)
[docs]
def pluralize(word: str) -> str:
"""
Convert a card type to its plural form, preserving capitalization.
Parameters
----------
word : str
Singular form of card type
Returns
-------
plural : str
Plural form with proper capitalization
"""
word_lower = word.lower()
match word_lower:
case _ if word_lower.endswith("y"):
# Most words ending in 'y' -> 'ies'
result = word_lower[:-1] + "ies"
case _ if word_lower.endswith(("s", "sh", "ch", "x", "z")):
result = word_lower + "es"
case _:
result = word_lower + "s"
# Capitalize if original was capitalized
if word and word[0].isupper():
return result.capitalize()
return result
[docs]
def extract_print_and_card_id(text: str) -> tuple[int, str] | None:
"""
Extract print_id and card_id from formatted deck list item.
Parameters
----------
text : str
List item text containing ⟨print_id:card_id⟩
Returns
-------
ids : tuple of (int, str) or None
(print_id, card_id) or None if not found
"""
if "⟨" in text and "⟩" in text:
ids_part = text.split("⟨")[-1].rstrip("⟩")
print_id_str, card_id = ids_part.split(":")
return int(print_id_str), card_id
logger.warning(f"Could not extract IDs from: {text}")
return None
[docs]
class FilteredCardList(ScrollableListBox):
"""Listbox showing filtered card search results."""
[docs]
def __init__(self, master: tk.Widget, repository):
super().__init__(master, selectmode="browse")
self._repository = repository
self._filter_query = ""
self._filter_options: "FilterOptions | None" = None
self._card_ids: list[str] = []
[docs]
def set_filter(self, query: str) -> None:
"""
Set filter query and refresh display.
Uses Scryfall-style query language to parse search terms.
Parameters
----------
query : str
Search query string (supports field:value, comparisons, etc.)
"""
self._filter_query = query.strip()
self.refresh()
[docs]
def set_filter_options(self, filter_options: "FilterOptions | None") -> None:
"""
Set filter options and refresh display.
Parameters
----------
filter_options : FilterOptions or None
Filter options containing property constraints
"""
self._filter_options = filter_options
self.refresh()
[docs]
def refresh(self) -> None:
self.clear()
self._card_ids = []
# Parse the search query using the query language
text_query, parsed_filters = parse_and_build_query(self._filter_query)
# Merge with dialog filters (dialog filters take precedence)
combined_filters = dict(parsed_filters)
if self._filter_options and self._filter_options.has_filters():
combined_filters.update(self._filter_options.filters)
# Convert combined filters back to FilterOptions-like dict, or None if empty
filter_dict = combined_filters if combined_filters else None
filtered = self._repository.filter_cards(text_query, filter_dict)
for card in filtered:
display_name = format_card_display_name(card)
self.insert(tk.END, display_name)
self._card_ids.append(card["card_id"])
[docs]
def get_selected_card_id(self) -> str | None:
"""
Get card_id of currently selected item.
Returns
-------
card_id : str or None
Card ID or None if nothing selected
"""
sel = self.get_selection()
if not sel:
return None
index = sel[0]
if 0 <= index < len(self._card_ids):
return self._card_ids[index]
return None
[docs]
class DeckCardList(ScrollableListBox):
"""Listbox showing cards in a deck (Fate, Dynasty, or Setup)."""
[docs]
def __init__(self, master: tk.Widget, repository, side: str):
super().__init__(master, selectmode="browse")
self._repository = repository
self._side = side
self._item_data: list[tuple[int | None, str]] = []
[docs]
def refresh(self, deck_state) -> None:
"""
Refresh display from deck state, grouped by card type.
Parameters
----------
deck_state : DeckState
Current deck state
"""
self.clear()
self._item_data = []
# Group cards by type
cards_by_type: dict[str, list[tuple[str, list[tuple[int, int]]]]] = {}
for card_id, print_list in deck_state.cards.items():
card = self._repository.get_card(card_id)
if not card:
continue
if not card_in_side(card, self._side):
continue
# Group by type
types = card.get("types") or []
card_type = types[0] if types else "Unknown"
if card_type not in cards_by_type:
cards_by_type[card_type] = []
cards_by_type[card_type].append((card_id, print_list))
# Display cards grouped by type
for card_type in sorted(cards_by_type.keys()):
cards_in_type = cards_by_type[card_type]
# Calculate total count for this type
type_total = sum(
sum(count for _, count in print_list) for _, print_list in cards_in_type
)
# Add type header with count first and proper plural
plural_type = pluralize(card_type)
type_header = f"{type_total}x {plural_type}"
self.insert(tk.END, type_header)
self._item_data.append((None, None)) # Type header has no card
# Add cards under this type
for card_id, print_list in sorted(cards_in_type):
card = self._repository.get_card(card_id)
if not card:
continue
total_count = sum(count for _, count in print_list)
display_name = format_card_display_name(card)
# If only one print, show on one line (indented under type)
if len(print_list) == 1:
print_id, count = print_list[0]
prints = self._repository.get_prints(card_id)
print_info = next((p for p in prints if p["print_id"] == print_id), None)
set_name = print_info.get("set_name") if print_info else None
if set_name:
entry = f" {total_count}x {display_name} [{set_name}]"
else:
entry = f" {total_count}x {display_name}"
self.insert(tk.END, entry)
self._item_data.append((print_id, card_id))
else:
# Multiple prints - show hierarchical view (indented under type)
entry = f" {total_count}x {display_name}"
self.insert(tk.END, entry)
self._item_data.append((None, card_id))
# Sub-entries for each print (double indented)
for print_id, count in sorted(print_list):
prints = self._repository.get_prints(card_id)
print_info = next((p for p in prints if p["print_id"] == print_id), None)
set_name = print_info.get("set_name") if print_info else "Unknown"
sub_entry = f" {count}x {set_name}"
self.insert(tk.END, sub_entry)
self._item_data.append((print_id, card_id))
def _add_deck_entry(self, card: dict, card_id: str, print_id: int, count: int) -> None:
"""DEPRECATED: No longer used, kept for compatibility."""
pass
[docs]
def get_selected_ids(self) -> tuple[int, str] | None:
"""
Get print_id and card_id of currently selected item.
Returns
-------
ids : tuple of (int, str) or None
(print_id, card_id) or None if nothing selected
"""
sel = self.get_selection()
if not sel:
return None
index = sel[0]
if 0 <= index < len(self._item_data):
print_id, card_id = self._item_data[index]
# Type headers have (None, None) - can't be removed
if card_id is None:
return None
# If print_id is None (card entry with multiple prints), use the first print
if print_id is None:
# Find the first sub-entry for this card
for i in range(index + 1, len(self._item_data)):
pid, cid = self._item_data[i]
if cid == card_id and pid is not None:
return (pid, card_id)
return (print_id, card_id) if print_id is not None else None
return None
[docs]
def get_total_count(self, deck_state, cards_by_id: dict) -> int:
"""
Get total card count for this side.
Parameters
----------
deck_state : DeckState
Current deck state
cards_by_id : dict of str to dict
Card lookup by ID
Returns
-------
count : int
Total cards
"""
return deck_state.get_card_count(self._side, cards_by_id)