# -*- coding: utf-8 -*-
"""
Contains helper data, functions, and classes for UI creation & management.
"""
import abc
import sys
from typing import *
import wx

import stat_stuff

# How many pixels to pad controls with.
CTRL_BORDER = 4
# Standard minimum sizes for things, given wx.Size values when the main form initializes.
BIG_MULTI_TEXTBOX_SIZE = None
MULTI_TEXTBOX_SIZE = None
NAME_TEXTBOX_SIZE = None
RECHARGE_TEXTBOX_SIZE = None
LANG_EQUIP_TEXTBOX_SIZE = None
SMALL_TEXTBOX_SIZE = None
MEDIUM_TEXTBOX_SIZE = None
X_BUTTON_SIZE = None
UP_DOWN_BUTTON_SIZE = None
SKILLS_WINDOW_SIZE = None
SENSES_WINDOW_SIZE = None
RESIST_VULN_WINDOW_SIZE = None
IMMUNE_WINDOW_SIZE = None
TRAIT_ACTION_WINDOW_SIZE = None
KEYWORD_CHECKLIST_SIZE = None
MONSTER_LIST_SIZE = None
MONSTER_LIST_MIN_HEIGHT = 200
MONSTER_LIST_MIN_WIDTH = 200
MONSTER_CTRLS_HEIGHT = 100
XP_TEXTBOX_SIZE = None
# Standard minimum sizes in dialog units, intended to be passed to one of the basic_...()
# UI helper functions that automatically call DLG_UNIT.
# DLG_UNIT(x) = x * charWidth / 4, DLG_UNIT(y) = y * charHeight / 8
LABEL_SMALL_DLG_SIZE = wx.Size(25, 12)
LABEL_MED_DLG_SIZE = wx.Size(60, 12)
LABEL_LARGE_DLG_SIZE = wx.Size(100, 12)
LABEL_STAT_DLG_SIZE = wx.Size(30, 12)
TEXT_SMALL_DLG_SIZE = wx.Size(20, 12)
TEXT_MED_DLG_SIZE = wx.Size(50, 12)
TEXT_LARGE_DLG_SIZE = wx.Size(250, 12)
TEXT_ACTION_TRAIT_NAME_DLG_SIZE = wx.Size(80, 12)
CHECK_SMALL_DLG_SIZE = wx.Size(40, 12)
CHECK_MED_DLG_SIZE = wx.Size(80, 12)
CHECK_LARGE_DLG_SIZE = wx.Size(120, 12)
DROPDOWN_SMALL_DLG_SIZE = wx.Size(22, 12)
DROPDOWN_MED_DLG_SIZE = wx.Size(50, 12)
DROPDOWN_LARGE_DLG_SIZE = wx.Size(150, 12)
CHECKLIST_MED_DLG_SIZE = wx.Size(150, 50)
CHECKLIST_KEYWORD_DLG_SIZE = wx.Size(-1, 50)
MULTILINE_ACTION_TRAIT_DLG_SIZE = wx.Size(100, 40)


TEXT_FONT = None  # Default font.
CTRL_FONT = None  # Font for stuff that is just a UI control (like the "X" delete button for actions and traits).
SCROLL_HEIGHT = 20  # Default amount scrolled windows scroll by.
STATUS_MESSAGE_TIME = 10000  # How many milliseconds to display a standard info message on the status bar before clearing it.

# String to tack on to text box labels whose text box contents support basic formatting markup.
TEXT_FORMATTING_HELP = " (supports *bold*, `italic`, and _underline_ markup)"

# Strings that may be shown on the UI to help explain monster role mods.
ROLE_MOD_HELP = {
    "Normal": "",
    "Elite": "200% HP, 200% XP, +2 all attributes, Saving Throws +2, Action Points 1",
    "Solo": "400% HP, 400% XP, +4 all attributes, Saving Throws +5, Action Points 2",
    "Minion": "1 HP, 25%/20%/17% XP by tier, missed attacks never damage a minion",
    "Super Minion": "33% HP, 33%/25%/20% XP by tier, damaging attacks that don't reduce HP to 0 deal it Bloodied damage instead",
}
SWARM_MOD_HELP = "Takes reduced damage from Melee and Ranged attacks, increased damage from Area and Close attacks"
# Text of the standard Swarm trait.
SWARM_TRAIT_DESCRIPTION = "The swarm can occupy the same space as another creature, and an enemy can enter its space, which is difficult terrain. The swarm cannot be pulled, pushed, or slid by melee or ranged attacks. It can squeeze through any opening that is large enough for at least one of the creatures it comprises."
# Starter text for a typical Swarm aura "attack".
SWARM_AURA_TEMPLATE = "Each enemy that [starts its turn in/ends its turn in/enters/is in] the aura [takes X damage/is slowed/takes a -X penalty to melee and ranged attack rolls]."

# ----------
# Logging functions.
# ----------


def log_error(message, *args):
    """
    Logs a message to stderr.

    :param message: The message to output.
        May use str.format()-style items, args will be applied to message accordingly.
    :param args: Arguments to apply to message via a call to str.format().
    """
    sys.stderr.write(message.format(*args) + '\n')
    sys.stderr.flush()


def log_info(message, *args):
    """
    Logs a message to stdout.

    :param message: The message to output.
        May use str.format()-style items, args will be applied to message accordingly.
    :param args: Arguments to apply to message via a call to str.format().
    """
    sys.stdout.write(message.format(*args) + '\n')
    sys.stdout.flush()

# ----------
# Abstract base classes.
# ----------


class BuilderFrame(wx.Frame):
    """
    Abstract base class of any Frame used in the application.

    This way children of the application's frame can guarantee their parent has certain methods,
    regardless of which style of builder is being presented.
    """
    __metaclass__ = abc.ABCMeta

    # Setup and initialization methods
    # --------------------------------

    @abc.abstractmethod
    def _setup_ui(self):
        raise NotImplementedError("_setup_ui must be defined to inherit from BuilderFrame")

    @abc.abstractmethod
    def _initialize(self):
        raise NotImplementedError("_initialize must be defined to inherit from BuilderFrame")

    # Action and helper methods
    # -------------------------

    @abc.abstractmethod
    def clear_status(self):
        raise NotImplementedError("clear_status must be defined to inherit from BuilderFrame")

    @abc.abstractmethod
    def layout(self):
        raise NotImplementedError("layout must be defined to inherit from BuilderFrame")

    @abc.abstractmethod
    def repaint(self):
        raise NotImplementedError("repaint must be defined to inherit from BuilderFrame")

    @abc.abstractmethod
    def resize(self):
        raise NotImplementedError("resize must be defined to inherit from BuilderFrame")

    @abc.abstractmethod
    def set_status(self, status_text: str, clear_later: bool = True):
        raise NotImplementedError("set_status must be defined to inherit from BuilderFrame")

# ----------
# Initialization functions.
# ----------


def setup_ui_helper_constants(char_width: int, char_height: int):
    """
    Initializes the "constant" global control size and font variables in this module if they haven't been yet.

    :param char_width: Integer average character width as reported by the calling window's GetCharWidth().
    :param char_height: Integer character height as reported by the calling window's GetCharHeight().
    """
    global BIG_MULTI_TEXTBOX_SIZE, MULTI_TEXTBOX_SIZE, NAME_TEXTBOX_SIZE, RECHARGE_TEXTBOX_SIZE
    global LANG_EQUIP_TEXTBOX_SIZE, SMALL_TEXTBOX_SIZE, MEDIUM_TEXTBOX_SIZE, X_BUTTON_SIZE, UP_DOWN_BUTTON_SIZE
    global SKILLS_WINDOW_SIZE, SENSES_WINDOW_SIZE, RESIST_VULN_WINDOW_SIZE, IMMUNE_WINDOW_SIZE
    global TRAIT_ACTION_WINDOW_SIZE, KEYWORD_CHECKLIST_SIZE, MONSTER_LIST_SIZE, XP_TEXTBOX_SIZE
    global TEXT_FONT, CTRL_FONT

    if BIG_MULTI_TEXTBOX_SIZE is None:
        BIG_MULTI_TEXTBOX_SIZE = wx.Size(char_width * 50, char_height * 10)
    if MULTI_TEXTBOX_SIZE is None:
        MULTI_TEXTBOX_SIZE = wx.Size(char_width * 50, char_height * 4)
    if NAME_TEXTBOX_SIZE is None:
        NAME_TEXTBOX_SIZE = wx.Size(char_width * 20, wx.DefaultCoord)
    if RECHARGE_TEXTBOX_SIZE is None:
        RECHARGE_TEXTBOX_SIZE = wx.Size(char_width * 8, wx.DefaultCoord)
    if LANG_EQUIP_TEXTBOX_SIZE is None:
        LANG_EQUIP_TEXTBOX_SIZE = wx.Size(char_width * 40, wx.DefaultCoord)
    if SMALL_TEXTBOX_SIZE is None:
        SMALL_TEXTBOX_SIZE = wx.Size(char_width * 6, wx.DefaultCoord)
    if MEDIUM_TEXTBOX_SIZE is None:
        MEDIUM_TEXTBOX_SIZE = wx.Size(char_width * 15, wx.DefaultCoord)
    if X_BUTTON_SIZE is None:
        X_BUTTON_SIZE = wx.Size(char_width * 3, char_height * 2)
    if UP_DOWN_BUTTON_SIZE is None:
        UP_DOWN_BUTTON_SIZE = wx.Size(char_width * 3, char_height * 2)
    if SKILLS_WINDOW_SIZE is None:
        SKILLS_WINDOW_SIZE = wx.Size(wx.DefaultCoord, char_height * 5)
    if SENSES_WINDOW_SIZE is None:
        SENSES_WINDOW_SIZE = wx.Size(char_width * 25, char_height * 14)
    if RESIST_VULN_WINDOW_SIZE is None:
        RESIST_VULN_WINDOW_SIZE = wx.Size(wx.DefaultCoord, char_height * 5)
    if IMMUNE_WINDOW_SIZE is None:
        IMMUNE_WINDOW_SIZE = wx.Size(wx.DefaultCoord, char_height * 5)
    if TRAIT_ACTION_WINDOW_SIZE is None:
        TRAIT_ACTION_WINDOW_SIZE = wx.Size(wx.DefaultCoord, char_height * 22)
    if KEYWORD_CHECKLIST_SIZE is None:
        KEYWORD_CHECKLIST_SIZE = wx.Size(wx.DefaultCoord, char_height * 5)
    if MONSTER_LIST_SIZE is None:
        MONSTER_LIST_SIZE = wx.Size(wx.DefaultCoord, char_height * 30)
    if XP_TEXTBOX_SIZE is None:
        XP_TEXTBOX_SIZE = wx.Size(char_width * 8, wx.DefaultCoord)
    # Set up fonts.
    if TEXT_FONT is None:
        TEXT_FONT = wx.Font(pointSize=10, family=wx.FONTFAMILY_ROMAN, style=wx.FONTSTYLE_NORMAL, weight=wx.FONTWEIGHT_NORMAL, underline=False)
    if CTRL_FONT is None:
        CTRL_FONT = wx.Font(pointSize=10, family=wx.FONTFAMILY_DEFAULT, style=wx.FONTSTYLE_NORMAL, weight=wx.FONTWEIGHT_NORMAL, underline=False)

# ----------
# Functions to grab values from the UI.
# ----------


def get_dropdown_str(dropdown: wx.Choice) -> str:
    """
    Convenience function to grab the string value of a dropdown's selection.

    :param dropdown: wx.Choice object.
    :return: The string value of the current selection of the dropdown box.
    """
    index = dropdown.GetCurrentSelection()
    if index == wx.NOT_FOUND:
        dropdown_str = ""
    else:
        dropdown_str = dropdown.GetString(index)

    return dropdown_str


def get_textbox_attribute(textbox: wx.TextCtrl) -> int:
    """
    Convenience function to get the integer value of an attribute text box.

    :param textbox: wx.TextCtrl object.
    :return: Integer value of the textbox, or DEFAULT_ATTR_VAL
        if the text box's value can't be parsed as an int.
    """
    try:
        attr_val = int(textbox.GetValue())
    except ValueError:
        attr_val = stat_stuff.DEFAULT_ATTR_VAL

    return attr_val


def get_textbox_int(textbox: Union[wx.TextEntry, wx.SpinCtrl]) -> int:
    """
    Convenience function to get the integer value of a text box.

    :param textbox: wx.TextCtrl object, or something that supports GetValue().
    :return: Integer value of the textbox, or 0
        if the text box's value can't be parsed as an int.
    """
    try:
        val = int(textbox.GetValue())
    except ValueError:
        val = 0

    return val


# ----------
# Control creation helper functions.
# ----------


def basic_button(parent: wx.Window, label: str, sizer: wx.Sizer, minsize: Optional[wx.Size] = None, style: int = 0, proportion: int = 0, flag: int = wx.ALL, border: int = CTRL_BORDER) -> wx.Button:
    """
    Creates a wx.Button object with "normal" parameters and adds it to the given sizer.

    :param parent: wx.Window serving as the parent.
    :param label: The initial text of the button.
    :param sizer: wx.Sizer supporting an Add() method similar to wx.BoxSizer.
    :param minsize: If not None, a wx.Size object in dialog units specifying minimum width and height for the button.
    :param style: OR'd set of Style flags.
    :param proportion: Proportion argument for the sizer.
    :param flag: Flags to pass to the sizer when adding the control.
    :param border: Size of the padding between this and other controls.
    :return: A wx.StaticText object set up with the appropriate parent & label.
    """
    button = wx.Button(parent=parent, id=wx.ID_ANY, label=label, style=style)
    if minsize is not None:
        button.SetMinSize(wx.DLG_UNIT(button, minsize))
    sizer.Add(window=button, proportion=proportion, flag=flag, border=border)
    return button


def basic_label(parent: wx.Window, label: str, sizer: wx.Sizer, minsize: Optional[wx.Size] = None, style: int = 0, proportion: int = 0, flag: int = wx.ALL, border: int = CTRL_BORDER) -> wx.StaticText:
    """
    Creates a wx.StaticText object with "normal" parameters and adds it to the given sizer.

    :param parent: wx.Window serving as the parent.
    :param label: The initial text of the label.
    :param sizer: wx.Sizer supporting an Add() method similar to wx.BoxSizer.
    :param minsize: Minimum desired width and height of the label in dialog units (will be passed to DLG_UNIT).
    :param style: OR'd set of Style flags.
    :param proportion: Proportion argument for the sizer.
    :param flag: Flags to pass to the sizer when adding the control.
    :param border: Size of the padding between this and other controls.
    :return: A wx.StaticText object set up with the appropriate parent & label.
    """
    label = wx.StaticText(parent=parent, id=wx.ID_ANY, label=label, style=style)
    if minsize is not None:
        label.SetMinSize(wx.DLG_UNIT(label, minsize))
    sizer.Add(window=label, proportion=proportion, flag=flag, border=border)
    return label


def basic_textbox(parent: wx.Window, value: str, sizer: wx.Sizer, minsize: Optional[wx.Size] = None, style: int = 0, proportion: int = 0, flag: int = wx.ALL, border: int = CTRL_BORDER) -> wx.TextCtrl:
    """
    Creates a wx.TextCtrl object with "normal" parameters and adds it to the given sizer.

    :param parent: wx.Window serving as the parent.
    :param value: The initial contents of the text box.
    :param sizer: wx.Sizer supporting an Add() method similar to wx.BoxSizer.
    :param minsize: If not None, a wx.Size object in dialog units to apply to the minimum size of the text box.
    :param style: OR'd set of Style flags.
    :param proportion: Proportion argument for the sizer.
    :param flag: Flags to pass to the sizer when adding the control.
    :param border: Size of the padding between this and other controls.
    :return: A wx.StaticText object set up with the appropriate parent & value.
    """
    textbox = wx.TextCtrl(parent=parent, id=wx.ID_ANY, value=value, style=style)
    if minsize is not None:
        textbox.SetMinSize(wx.DLG_UNIT(textbox, minsize))
    sizer.Add(window=textbox, proportion=proportion, flag=flag, border=border)
    return textbox


def labeled_textbox(parent: wx.Window, label: str, value: str, sizer: wx.Sizer, label_minsize: Optional[wx.Size] = None, text_minsize: Optional[wx.Size] = None, label_style: int = 0, text_style: int = 0, label_prop: int = 0, text_prop: int = 0, label_flag: int = wx.ALL, text_flag: int = wx.ALL, label_border: int = CTRL_BORDER, text_border: int = CTRL_BORDER) -> tuple:
    """
    Creates wx.StaticText and wx.TextCtrl objects with "normal" parameters and adds them to the given sizer.
    The StaticText object will come before the TextCtrl object and serve as its label.

    :param parent: wx.Window serving as the parent.
    :param label: The text for the label.
    :param value: The initial contents of the text box.
    :param sizer: wx.Sizer supporting an Add() method similar to wx.BoxSizer.
    :param label_minsize: If not None, a wx.Size object in dialog units to apply to the minimum size of the label.
    :param text_minsize: If not None, a wx.Size object in dialog units to apply to the minimum size of the text box.
    :param label_style: OR'd set of Style flags for the label.
    :param text_style: OR'd set of Style flags for the text box.
    :param label_prop: Proportion argument for the sizer for the label.
    :param text_prop: Proportion argument for the sizer for the text box.
    :param label_flag: Flags to pass to the sizer when adding the label.
    :param text_flag: Flags to pass to the sizer when adding the text box.
    :param label_border: Size of the padding between the label and other controls.
    :param text_border: Size of the padding between the text box and other controls.
    :return: A tuple of the wx.StaticText object and wx.TextCtrl objects set up with appropriate values.
    """
    label = wx.StaticText(parent=parent, id=wx.ID_ANY, label=label, style=label_style)
    textbox = wx.TextCtrl(parent=parent, id=wx.ID_ANY, value=value, style=text_style)
    if label_minsize is not None:
        label.SetMinSize(wx.DLG_UNIT(label, label_minsize))
    if text_minsize is not None:
        textbox.SetMinSize(wx.DLG_UNIT(textbox, text_minsize))
    sizer.Add(window=label, proportion=label_prop, flag=label_flag, border=label_border)
    sizer.Add(window=textbox, proportion=text_prop, flag=text_flag, border=text_border)
    return label, textbox


def basic_checkbox(parent: wx.Window, label: str, sizer: wx.Sizer, minsize: Optional[wx.Size] = None, style: int = 0, proportion: int = 0, flag: int = wx.ALL, border: int = CTRL_BORDER) -> wx.CheckBox:
    """
    Creates a wx.CheckBox object with "normal" parameters and adds it to the given sizer.

    :param parent: wx.Window serving as the parent.
    :param label: The caption of the checkbox.
    :param sizer: wx.Sizer supporting an Add() method similar to wx.BoxSizer.
    :param minsize: If present, a wx.Size object in dialog units specifying minimum size of the control.
    :param style: OR'd set of Style flags.
    :param proportion: Proportion argument for the sizer.
    :param flag: Flags to pass to the sizer when adding the control.
    :param border: Size of the padding between this and other controls.
    :return: A wx.CheckBox object set up with the appropriate parent & value.
    """
    checkbox = wx.CheckBox(parent=parent, id=wx.ID_ANY, label=label, style=style)
    if minsize is not None:
        checkbox.SetMinSize(wx.DLG_UNIT(checkbox, minsize))
    sizer.Add(window=checkbox, proportion=proportion, flag=flag, border=border)
    return checkbox


def basic_radiobutton(parent: wx.Window, label: str, sizer: wx.Sizer, minsize: Optional[wx.Size] = None, group_start: bool = False, style: int = 0, proportion: int = 0, flag: int = wx.ALL, border: int = CTRL_BORDER) -> wx.RadioButton:
    """
    Creates a wx.RadioButton object with "normal" parameters and adds it to the given sizer.

    :param parent: wx.Window serving as the parent.
    :param label: The caption of the radio button.
    :param sizer: wx.Sizer supporting an Add() method similar to wx.BoxSizer.
    :param minsize: If present, a wx.Size object in dialog units specifying minimum size of the control.
    :param group_start: True to include RB_GROUP in style and start a new radio group, False to include with last group.
    :param style: OR'd set of Style flags.
    :param proportion: Proportion argument for the sizer.
    :param flag: Flags to pass to the sizer when adding the control.
    :param border: Size of the padding between this and other controls.
    :return: A wx.CheckBox object set up with the appropriate parent & value.
    """
    radio_style = style
    if group_start:
        radio_style |= wx.RB_GROUP
    radio = wx.RadioButton(parent=parent, id=wx.ID_ANY, label=label, style=radio_style)
    if minsize is not None:
        radio.SetMinSize(wx.DLG_UNIT(radio, minsize))
    sizer.Add(window=radio, proportion=proportion, flag=flag, border=border)
    return radio


def basic_dropdown(parent: wx.Window, choices: list, sizer: wx.Sizer, minsize: Optional[wx.Size] = None, style: int = 0, proportion: int = 0, flag: int = wx.ALL, border: int = CTRL_BORDER) -> wx.Choice:
    """
    Creates a wx.Choice object with "normal" parameters and adds it to the given sizer.

    :param parent: wx.Window serving as the parent.
    :param choices: A list of initial choices for the dropdown.
    :param sizer: wx.Sizer supporting an Add() method similar to wx.BoxSizer.
    :param minsize: If not None, a wx.Size object in dialog units specifying the minimum size of the dropdown.
    :param style: OR'd set of Style flags.
    :param proportion: Proportion argument for the sizer.
    :param flag: Flags to pass to the sizer when adding the control.
    :param border: Size of the padding between this and other controls.
    :return: A wx.Choice object set up with the appropriate parent & list of choices.
    """
    dropdown = wx.Choice(parent=parent, id=wx.ID_ANY, choices=choices, style=style)
    if minsize is not None:
        dropdown.SetMinSize(wx.DLG_UNIT(dropdown, minsize))
    sizer.Add(window=dropdown, proportion=proportion, flag=flag, border=border)
    return dropdown


def basic_editable_dropdown(parent: wx.Window, choices: list, sizer: wx.Sizer, value: str = "", minsize: Optional[wx.Size] = None, style: int = 0, proportion: int = 0, flag: int = wx.ALL, border: int = CTRL_BORDER) -> wx.ComboBox:
    """
    Creates a wx.ComboBox object with "normal" parameters and adds it to the given sizer.
    :param parent: wx.Window serving as the parent.
    :param choices: A list of initial choices for the dropdown.
    :param sizer: wx.Sizer supporting an Add() method similar to wx.BoxSizer.
    :param value: The initial value of the editable part of the dropdown.
    :param minsize: If not None, a wx.Size object in dialog units specify the minimum size of the dropdown.
    :param style: OR'd set of Style flags.
    :param proportion: Proportion argument for the sizer.
    :param flag: Flags to pass to the sizer when adding the control.
    :param border: Size of the padding between this and other controls.
    :return: A wx.ComboBox object set up with the appropriate parent & value.
    """
    dropdown = wx.ComboBox(parent=parent, id=wx.ID_ANY, value=value, choices=choices, style=style)
    if minsize is not None:
        dropdown.SetMinSize(wx.DLG_UNIT(dropdown, minsize))
    sizer.Add(window=dropdown, proportion=proportion, flag=flag, border=border)
    return dropdown


def basic_checklist(parent: wx.Window, choices: list, sizer: wx.Sizer, minsize: Optional[wx.Size] = None, style: int = 0, proportion: int = 0, flag: int = wx.ALL, border: int = CTRL_BORDER) -> wx.CheckListBox:
    """
    Creates a wx.CheckListBox object with "normal" parameters and adds it to the given sizer.

    :param parent: wx.Window serving as the parent.
    :param choices: A list of initial choices for the dropdown.
    :param sizer: wx.Sizer supporting an Add() method similar to wx.BoxSizer.
    :param minsize: If not None, a wx.Size object in dialog units specifying the minimum size of the checklist.
    :param style: OR'd set of Style flags.
    :param proportion: Proportion argument for the sizer.
    :param flag: Flags to pass to the sizer when adding the control.
    :param border: Size of the padding between this and other controls.
    :return: A wx.CheckListBox object set up with the appropriate parent & value.
    """
    checklist = wx.CheckListBox(parent=parent, id=wx.ID_ANY, choices=choices, style=style)
    if minsize is not None:
        checklist.SetMinSize(wx.DLG_UNIT(checklist, minsize))
    sizer.Add(window=checklist, proportion=proportion, flag=flag, border=border)
    return checklist
