# -*- coding: utf-8 -*-
"""
Contains helper data, functions, and classes for Monster Builder UI creation & management.
"""
# noinspection PyUnresolvedReferences
import wx

import ui_helpers
from ui_helpers import *
# Used to resolve circular reference in type hints.
import monster_builder

# ----------
# Control Creation Helper Classes and Functions
# ----------


class ResistVulnControls(wx.BoxSizer):
    """
    Specialized BoxSizer containing the controls needed to display options for a resistance/vulnerability/immunity.
    """

    def __init__(self, container: 'monster_builder.MonsterBuilderPanel', parent: wx.Window, label: str, value: str = "", has_rv=False, orient: int = wx.HORIZONTAL):
        """
        Creates a new set of resistance/vulnerability/immunity controls in a sizer.
        All controls get the wx naming convention "[R/V/I Name]:[Control]", where [R/V/I Name] is the name of the
        resistance/vulnerability/immunity item, and [Control] is what the control represents.

        :param container: MonsterBuilderPanel object that will hold the sizer. Used to reference callbacks.
        :param parent: The owner of the new controls.
        :param label: The label text prefacing the controls.
        :param value: The initial value in the resist/vuln text box.
        :param has_rv: If True, this can be a resistance/vulnerability. If False, it can only be an immunity.
        :param orient: The orientation of the sizer.
        """
        super().__init__(orient=orient)
        self.container = container
        self.parent = parent
        self.resist_vuln_name = label
        self.init_value = value
        self.has_rv = has_rv
        self.hidden = False

        self.checkbox = basic_checkbox(parent=self.parent, label=self.resist_vuln_name, sizer=self, flag=wx.ALL | wx.RESERVE_SPACE_EVEN_IF_HIDDEN, border=0)
        self.checkbox.SetName("{0}:Name".format(self.resist_vuln_name))
        self.checkbox.Bind(event=wx.EVT_CHECKBOX, handler=self.container.cb_resist_vuln_clicked)
        if self.has_rv:
            self.resist_radio = basic_radiobutton(parent=self.parent, label="Resist", sizer=self, group_start=True, flag=wx.ALL | wx.RESERVE_SPACE_EVEN_IF_HIDDEN, border=0)
            self.resist_radio.SetName("{0}:Resist".format(self.resist_vuln_name))
            self.resist_radio.SetValue(True)
            self.resist_radio.Bind(event=wx.EVT_RADIOBUTTON, handler=self.container.cb_resist_vuln_radio_clicked)
            self.vuln_radio = basic_radiobutton(parent=self.parent, label="Vuln", sizer=self, flag=wx.ALL | wx.RESERVE_SPACE_EVEN_IF_HIDDEN, border=0)
            self.vuln_radio.SetName("{0}:Vuln".format(self.resist_vuln_name))
            self.vuln_radio.Bind(event=wx.EVT_RADIOBUTTON, handler=self.container.cb_resist_vuln_radio_clicked)
            self.immune_radio = basic_radiobutton(parent=self.parent, label="Immune", sizer=self, flag=wx.ALL | wx.RESERVE_SPACE_EVEN_IF_HIDDEN, border=0)
        else:
            self.resist_radio = None
            self.vuln_radio = None
            self.immune_radio = basic_radiobutton(parent=self.parent, label="Immune", sizer=self, group_start=True, flag=wx.ALL | wx.RESERVE_SPACE_EVEN_IF_HIDDEN, border=0)
        # The immunity radio button always exists, just depends on if it's the start of the group or not.
        self.immune_radio.SetName("{0}:Immune".format(self.resist_vuln_name))
        self.immune_radio.Bind(event=wx.EVT_RADIOBUTTON, handler=self.container.cb_resist_vuln_radio_clicked)
        self.value_text = basic_textbox(parent=self.parent, value=self.init_value, sizer=self, minsize=ui_helpers.TEXT_SMALL_DLG_SIZE, flag=wx.ALL | wx.RESERVE_SPACE_EVEN_IF_HIDDEN, border=0)
        self.value_text.SetName("{0}:Value".format(self.resist_vuln_name))

    def get_rvi_type(self):
        """
        Returns "r", "v", or "i" based on which radio button is currently selected (resistance, vulnerability, or immunity).
        """
        if not self.has_rv or self.immune_radio.GetValue():
            return "i"
        elif self.vuln_radio.GetValue():
            return "v"
        else:
            return "r"

    def get_value(self):
        """
        Returns the string contents of the value textbox.
        """
        return self.value_text.GetValue()

    def get_int_value(self):
        """
        Returns the integer-cast contents of the value textbox, or 0 if its contents aren't an integer.
        """
        return ui_helpers.get_textbox_int(self.value_text)

    def repaint(self):
        """
        Adjusts the display of the child controls based on what's selected.
        """
        if self.immune_radio.GetValue():
            self.value_text.Disable()
            self.value_text.Hide()
            self.Hide(self.value_text)
        else:
            self.value_text.Enable()
            if not self.hidden:
                self.value_text.Show()
                self.Show(self.value_text)

    def hide(self):
        """
        Hides all child controls except the main checkbox.
        """
        if self.has_rv:
            self.resist_radio.Hide()
            self.vuln_radio.Hide()
            self.Hide(self.resist_radio)
            self.Hide(self.vuln_radio)
        self.immune_radio.Hide()
        self.value_text.Hide()
        self.Hide(self.immune_radio)
        self.Hide(self.value_text)
        self.hidden = True

    def show(self):
        """
        Shows all child controls.
        """
        if self.has_rv:
            self.resist_radio.Show()
            self.vuln_radio.Show()
            self.Show(self.resist_radio)
            self.Show(self.vuln_radio)
        self.immune_radio.Show()
        self.value_text.Show()
        self.Show(self.immune_radio)
        self.Show(self.value_text)
        self.hidden = False
        # This makes sure the text box only shows if it should.
        self.repaint()

    def clear(self):
        """
        Selects the resist radio button if available (immune otherwise) and clears the text box.
        """
        if self.has_rv:
            self.resist_radio.SetValue(True)
            self.vuln_radio.SetValue(False)
            self.immune_radio.SetValue(False)
        else:
            self.immune_radio.SetValue(True)
        self.value_text.SetValue("")
        self.repaint()


class SenseControls(wx.BoxSizer):
    """
    Specialized BoxSizer holding the controls needed to display a sense with optional range.
    """

    def __init__(self, label: str, has_value: bool, container: 'monster_builder.MonsterBuilderPanel', parent: wx.Window):
        """
        Creates a new set of sense controls in a sizer.

        :param label: What to label the "does(n't) have" checkbox.
        :param has_value: Whether or not to include an editable range textbox.
        :param container: MonsterBuilderPanel object that will hold the controls.
        :param parent: The owner of the new sense controls.
        """
        super().__init__(orient=wx.HORIZONTAL)
        self.container = container
        self.parent = parent
        self.label = label
        self.has_value = has_value

        self.checkbox = basic_checkbox(parent=parent, label=label, sizer=self, flag=wx.LEFT | wx.RIGHT)
        self.checkbox.SetName("{0:s}:Checkbox".format(self.label))
        self.checkbox.Bind(event=wx.EVT_CHECKBOX, handler=self._cb_sense_clicked)
        # If the sense has a value, add a text box to put that in.
        if has_value:
            self.textbox = basic_textbox(parent=parent, value="0", sizer=self, minsize=ui_helpers.TEXT_SMALL_DLG_SIZE, flag=wx.LEFT | wx.RIGHT)
            self.textbox.SetName("{0:s}:Value".format(self.label))
            self.textbox.Disable()
        else:
            self.textbox = None

    # noinspection PyUnusedLocal
    def _cb_sense_clicked(self, event):
        """
        Callback handling when a sense gets (un)checked.
        Enables/disables the sense's textbox if it has one.

        :param event: CommandEvent object containing details about the event.
        """
        self.update_ui()

    def update_ui(self):
        """
        Adjusts the UI for a sense based on whether it's checked or not.
        """
        if self.has_value:
            if self.checkbox.IsChecked():
                self.textbox.Enable()
            else:
                self.textbox.Disable()
        self.Layout()
        self.container.layout()


class SkillControls(object):
    """
    Container class holding the controls needed to display options for a skill.
    """

    def __init__(self, container: 'monster_builder.MonsterBuilderPanel', parent: wx.Window, sizer: wx.FlexGridSizer, label: str):
        """
        Creates a new set of controls for a skill as a new row of a FlexGridSizer.
        All controls get the wx naming convention "[Skill Name]:[Control]", where [Skill Name] is the name of the
        skill, and [Control] is what the control represents.

        :param container: MonsterBuilderPanel object that will hold the sizer. Used to reference callbacks.
        :param parent: The owner of the new controls.
        :param sizer: The FlexGridSizer to add a row of controls to.
        :param label: The label text prefacing the controls.
        """
        super().__init__()
        self.container = container
        self.parent = parent
        self.sizer = sizer
        self.skill_name = label
        # Which attribute (str, con, etc.) this skill uses as a modifier.
        self.attribute_control = self.container.textboxes.__getattribute__(stat_stuff.SKILLS[self.skill_name])
        # Suggested value for this skill calculated from monster stats.
        self.calc_value = 0

        self.included_checkbox = basic_checkbox(parent=self.parent, label="", sizer=self.sizer, flag=wx.ALL | wx.RESERVE_SPACE_EVEN_IF_HIDDEN, border=0)
        self.included_checkbox.SetName("{0}:Included".format(self.skill_name))
        self.trained_checkbox = basic_checkbox(parent=self.parent, label="", sizer=self.sizer, flag=wx.ALL | wx.RESERVE_SPACE_EVEN_IF_HIDDEN, border=0)
        self.trained_checkbox.SetName("{0}:Trained".format(self.skill_name))
        self.trained_checkbox.Bind(event=wx.EVT_CHECKBOX, handler=self.cb_trained_clicked)
        self.custom_checkbox = basic_checkbox(parent=self.parent, label="", sizer=self.sizer, flag=wx.ALL | wx.RESERVE_SPACE_EVEN_IF_HIDDEN, border=0)
        self.custom_checkbox.SetName("{0}:Custom".format(self.skill_name))
        self.custom_checkbox.Bind(event=wx.EVT_CHECKBOX, handler=self.cb_custom_clicked)
        self.label = basic_label(parent=self.parent, label=self.skill_name, sizer=self.sizer, flag=wx.ALL | wx.RESERVE_SPACE_EVEN_IF_HIDDEN, border=0)
        self.label.SetName("{0}:Label".format(self.skill_name))
        self.value_text = basic_textbox(parent=self.parent, value=str(self.calc_value), sizer=self.sizer, minsize=ui_helpers.TEXT_SMALL_DLG_SIZE, flag=wx.ALL | wx.RESERVE_SPACE_EVEN_IF_HIDDEN, border=0)
        self.value_text.SetName("{0}:Value".format(self.skill_name))

    # noinspection PyUnusedLocal
    def cb_custom_clicked(self, event: wx.CommandEvent):
        """
        Callback handling when the user (un)checks the option to use a custom skill value.

        :param event: CommandEvent object containing details about the event.
        """
        # If they checked custom, then auto-check include as well since that's probably what they want.
        if event.IsChecked():
            self.included_checkbox.SetValue(True)
        self.update_ui()

    # noinspection PyUnusedLocal
    def cb_trained_clicked(self, event: wx.CommandEvent):
        """
        Callback handling when the user (un)checks the option to consider a skill (un)trained.

        :param event: CommandEvent object containing details about the event.
        """
        # If they checked train, then auto-check include as well since that's probably what they want.
        if event.IsChecked():
            self.included_checkbox.SetValue(True)
        self.update_ui()

    def get_value(self) -> str:
        """
        Returns the string contents of the skill value text box.
        """
        return self.value_text.GetValue()

    def get_int_value(self) -> int:
        """
        Returns the value in the skill text box as an integer.
        """
        return ui_helpers.get_textbox_int(self.value_text)

    def is_custom(self) -> bool:
        """
        Returns True if the user is allowed to enter a custom skill value
        or False if it's clamped to the "recommended" value.
        """
        return self.custom_checkbox.IsChecked()

    def is_included(self) -> bool:
        """
        Returns True if the skill should be listed on the exported monster card,
        False if it should not be listed.
        """
        return self.included_checkbox.IsChecked()

    def is_trained(self) -> bool:
        """
        Returns True if this skill is considered trained, False otherwise.
        """
        return self.trained_checkbox.IsChecked()

    def set_label_value(self):
        """
        Updates the suggested value for the skill to the current calculated value.
        """
        self.label.SetLabel("{0:s} ({1:d})".format(self.skill_name, self.calc_value))

    def recalculate(self):
        """
        Recalculates the suggested skill value based on current monster stats.
        Also updates the skill label with the suggested value.
        """
        self.calc_value = stat_stuff.calc_attribute_mod(ui_helpers.get_textbox_attribute(self.attribute_control), int(ui_helpers.get_dropdown_str(self.container.dropdowns.level)))
        if self.is_trained():
            self.calc_value += 5
        self.set_label_value()

    def reset_value(self):
        """
        Resets the value in the textbox to the "default" value based on the monster's stats.
        """
        self.value_text.SetValue(str(self.calc_value))

    def update_ui(self):
        """
        Adjusts the display of the controls based on what's selected.
        """
        self.recalculate()
        if self.is_custom():
            self.value_text.Enable()
        else:
            self.value_text.Disable()
            self.reset_value()

    def reset(self):
        """
        Clears all checkboxes and resets the current value.
        """
        self.custom_checkbox.SetValue(False)
        self.included_checkbox.SetValue(False)
        self.trained_checkbox.SetValue(False)
        self.value_text.SetValue("")
        self.update_ui()


class SkillsPanel(wx.Panel):
    """
    Specialized Panel containing & managing controls to display options for monster skills.

    Access controls for each skill by doing something like this.skills["SkillName"].
    """

    def __init__(self, container: 'monster_builder.MonsterBuilderPanel', parent: wx.Window, label: str = "Skills"):
        """
        Creates the controls to display a list of editable skill values.

        :param container: MonsterBuilderPanel object that will hold this panel. Used to reference callbacks and other controls.
        :param parent: The owner of this panel.
        :param label: The name to show for the panel's box.
        """
        super().__init__(parent=parent, id=wx.ID_ANY)
        self.container = container
        self.parent = parent
        self.box_label = label
        self.sizer = wx.StaticBoxSizer(orient=wx.HORIZONTAL, parent=self, label=self.box_label)
        self.static_box = self.sizer.GetStaticBox()

        # Add specific options for each skill.
        self.skill_list = wx.ScrolledWindow(self.static_box, wx.ID_ANY, style=wx.VSCROLL, name="Skills:List")
        self.skill_list.SetScrollRate(wx.DefaultCoord, ui_helpers.SCROLL_HEIGHT)
        self.skill_list.SetVirtualSize(ui_helpers.SKILLS_WINDOW_SIZE)
        self.grid_sizer = wx.FlexGridSizer(cols=5, vgap=0, hgap=0)
        self.grid_sizer.AddGrowableCol(4)
        # Headers to explain the check boxes.
        self.included_label = basic_label(parent=self.skill_list, label="List", sizer=self.grid_sizer, flag=wx.RIGHT | wx.RESERVE_SPACE_EVEN_IF_HIDDEN)
        self.trained_label = basic_label(parent=self.skill_list, label="Train", sizer=self.grid_sizer, flag=wx.RIGHT | wx.RESERVE_SPACE_EVEN_IF_HIDDEN)
        self.custom_label = basic_label(parent=self.skill_list, label="Custom", sizer=self.grid_sizer, flag=wx.RIGHT | wx.RESERVE_SPACE_EVEN_IF_HIDDEN)
        self.grid_sizer.AddStretchSpacer(0)
        self.grid_sizer.AddStretchSpacer(0)
        # A row of controls for each skill.
        for skill in stat_stuff.SKILLS.keys():
            new_skill = SkillControls(container=self.container, parent=self.skill_list, sizer=self.grid_sizer, label=skill)
            self.container.skills[skill] = new_skill
        # Add a special binding for when Perception changes so the "core stats" panel's display can update too.
        self.container.skills["Perception"].value_text.Bind(wx.EVT_TEXT, self._cb_perception_change)
        # Permanently disable including Perception in the normal monster's skill list, since it's always shown.
        self.container.skills["Perception"].included_checkbox.SetValue(False)
        self.container.skills["Perception"].included_checkbox.Disable()

        self.skill_list.SetSizer(self.grid_sizer)
        self.sizer.Add(self.skill_list, flag=wx.EXPAND)
        self.SetSizer(self.sizer)

    # noinspection PyUnusedLocal
    def _cb_perception_change(self, event):
        """
        Callback handling when the user changes the monster's Perception value.

        :param event: CommandEvent object containing details about the event.
        """
        self._perception_changed()

    def _perception_changed(self):
        """
        Handles updating helper UI fields when the monster's Perception changes.
        """
        self.container.textboxes.perception.SetValue(self.container.skills["Perception"].get_value())

    def get(self, skill: str) -> str:
        """
        Retrieves the current value for the given skill.

        :param skill: String name of skill.
        :return: Value from the textbox of that skill.
        """
        return self.container.skills[skill].get_value()

    def get_int(self, skill: str) -> int:
        """
        Retrieves the current value for the given skill.

        :param skill: String name of skill.
        :return: Integer value from the textbox of that skill.
        """
        return self.container.skills[skill].get_int_value()

    def recalculate(self):
        """
        Recalculates all skills' suggested values.
        """
        for skill in self.container.skills.values():
            skill.recalculate()

    def reset(self):
        """
        Resets all skills to their default states.
        """
        for skill in self.container.skills.values():
            skill.reset()
        self._perception_changed()

    def reset_values(self):
        """
        Resets the value in each skill's text box to the current suggested value.
        """
        for skill in self.container.skills.values():
            skill.reset_value()
        self._perception_changed()

    def set_label_values(self):
        """
        Updates all skills' labels to show the current suggested value for them.
        """
        for skill in self.container.skills.values():
            skill.set_label_value()

    def update(self):
        """
        Updates the display of all skills, presumably after something like a monster being loaded from file.
        """
        for skill in self.container.skills.values():
            skill.update_ui()
        self._perception_changed()


class KeywordControls(wx.BoxSizer):
    """
    Specialized BoxSizer holding controls to select keywords for traits & actions.
    """

    def __init__(self, container: 'monster_builder.MonsterBuilderPanel', parent: wx.Window, parent_type: str, parent_id: int):
        """
        Creates a set of multi-select dropdowns to attach keywords to the given trait/action.
        :param container: MonsterBuilderPanel the controls will be part of.
        :param parent: ActionPanel or TraitPanel containing the controls.
        :param parent_type: "Action" or "Trait", used to name the controls.
        :param parent_id: The ID of the trait or action, will become part of the name of these controls.
        """
        super().__init__(orient=wx.HORIZONTAL)
        self.container = container
        self.parent = parent
        self.parent_type = parent_type
        self.parent_id = parent_id

        self.damage_checklist = basic_checklist(parent=self.parent, choices=stat_stuff.KEYWORDS_DAMAGES, sizer=self, minsize=ui_helpers.CHECKLIST_KEYWORD_DLG_SIZE)
        self.damage_checklist.SetSize(x=wx.DefaultCoord, y=wx.DefaultCoord, width=ui_helpers.KEYWORD_CHECKLIST_SIZE.GetWidth(), height=ui_helpers.KEYWORD_CHECKLIST_SIZE.GetHeight())
        self.damage_checklist.SetName("{0:s}KeywordsDamage:{1:d}".format(self.parent_type, self.parent_id))
        self.source_checklist = basic_checklist(parent=self.parent, choices=stat_stuff.KEYWORDS_SOURCES, sizer=self, minsize=ui_helpers.CHECKLIST_KEYWORD_DLG_SIZE)
        self.source_checklist.SetSize(x=wx.DefaultCoord, y=wx.DefaultCoord, width=ui_helpers.KEYWORD_CHECKLIST_SIZE.GetWidth(), height=ui_helpers.KEYWORD_CHECKLIST_SIZE.GetHeight())
        self.source_checklist.SetName("{0:s}KeywordsSource:{1:d}".format(self.parent_type, self.parent_id))
        self.type_checklist = basic_checklist(parent=self.parent, choices=stat_stuff.KEYWORDS_TYPES, sizer=self, minsize=ui_helpers.CHECKLIST_KEYWORD_DLG_SIZE)
        self.type_checklist.SetSize(x=wx.DefaultCoord, y=wx.DefaultCoord, width=ui_helpers.KEYWORD_CHECKLIST_SIZE.GetWidth(), height=ui_helpers.KEYWORD_CHECKLIST_SIZE.GetHeight())
        self.type_checklist.SetName("{0:s}KeywordsType:{1:d}".format(self.parent_type, self.parent_id))

    def set_id(self, new_id: int):
        """
        Change the ID of these controls to match a new ID of their parent panel.

        :param new_id: Integer ID to change to.
        """
        self.parent_id = new_id
        self.damage_checklist.SetName("{0:s}KeywordsDamage:{1:d}".format(self.parent_type, self.parent_id))
        self.source_checklist.SetName("{0:s}KeywordsSource:{1:d}".format(self.parent_type, self.parent_id))
        self.type_checklist.SetName("{0:s}KeywordsType:{1:d}".format(self.parent_type, self.parent_id))


class ActionPanel(wx.Panel):
    """
    Container class for the controls needed to display an editable action.
    """
    # Static ID given to the next action panel when it is created. Auto-incremented after panel creation.
    next_action_id = 0

    def __init__(self, container: 'monster_builder.MonsterBuilderPanel', parent: wx.Window, id: int = wx.ID_ANY, pos: wx.Point = wx.DefaultPosition, size: wx.Size = wx.DefaultSize, style: int = wx.TAB_TRAVERSAL):
        """
        Creates a new set of action controls in a panel.
        Panel's wx name is always "ActionPanel:X" where X is the action ID.

        :param container: MonsterBuilderPanel object that will hold the panel.
        :param parent: The owner of the new action panel.
        :param id: Integer ID of the panel for wx.
        :param pos: Position of the panel.
        :param size: Size of the panel.
        :param style: Style flags for the panel.
        """
        super().__init__(parent=parent, id=id, pos=pos, size=size, style=style, name="ActionPanel:{0:d}".format(self.next_action_id))
        self.container = container
        self.parent = parent
        self.action_id = ActionPanel.next_action_id

        self.sizer = wx.BoxSizer(orient=wx.VERTICAL)

        main_row_sizer = wx.BoxSizer(orient=wx.HORIZONTAL)

        # Buttons to move the action up and down in the list.
        order_sizer = wx.BoxSizer(orient=wx.VERTICAL)
        self.up_button = wx.Button(parent=self, id=wx.ID_ANY, label="^", size=ui_helpers.UP_DOWN_BUTTON_SIZE)
        self.up_button.SetName("ActionUp:{0:d}".format(self.action_id))
        self.up_button.SetFont(ui_helpers.CTRL_FONT)
        self.up_button.Bind(wx.EVT_BUTTON, self._cb_move_up)
        order_sizer.Add(window=self.up_button, proportion=0, flag=wx.ALL | wx.ALIGN_LEFT, border=0)
        self.down_button = wx.Button(parent=self, id=wx.ID_ANY, label="v", size=ui_helpers.UP_DOWN_BUTTON_SIZE)
        self.down_button.SetName("ActionDown:{0:d}".format(self.action_id))
        self.down_button.SetFont(ui_helpers.CTRL_FONT)
        self.down_button.Bind(wx.EVT_BUTTON, self._cb_move_down)
        order_sizer.Add(window=self.down_button, proportion=0, flag=wx.ALL | wx.ALIGN_LEFT, border=0)
        main_row_sizer.Add(sizer=order_sizer, flag=wx.EXPAND)

        left_col_sizer = wx.BoxSizer(orient=wx.VERTICAL)

        # Upper left row of controls.
        upper_row_sizer = wx.BoxSizer(orient=wx.HORIZONTAL)
        self.type_box = basic_dropdown(parent=self, choices=["Standard", "Move", "Minor", "Triggered", "Free", "Other"], sizer=upper_row_sizer)
        self.type_box.SetName("ActionType:{0:d}".format(self.action_id))
        self.range_type_box = basic_dropdown(parent=self, choices=["", "Melee", "Melee Basic", "Ranged", "Ranged Basic", "Burst", "Blast"], sizer=upper_row_sizer)
        self.range_type_box.SetName("ActionRangeType:{0:d}".format(self.action_id))
        self.name_label = basic_label(parent=self, label="Name", sizer=upper_row_sizer)
        self.name_label.SetName("ActionNameLabel:{0:d}".format(self.action_id))
        self.name_text = basic_textbox(parent=self, value="", sizer=upper_row_sizer, minsize=ui_helpers.TEXT_ACTION_TRAIT_NAME_DLG_SIZE)
        self.name_text.SetName("ActionName:{0:d}".format(self.action_id))
        self.frequency_box = basic_dropdown(parent=self, choices=["", "At-Will", "Encounter", "Recharge"], sizer=upper_row_sizer)
        self.frequency_box.SetName("ActionFrequency:{0:d}".format(self.action_id))
        self.frequency_box.Bind(wx.EVT_CHOICE, self._cb_change_frequency)
        self.frequency_text = basic_textbox(parent=self, value="", sizer=upper_row_sizer, minsize=ui_helpers.TEXT_MED_DLG_SIZE)
        self.frequency_text.SetName("ActionFrequencyText:{0:d}".format(self.action_id))
        left_col_sizer.Add(sizer=upper_row_sizer, flag=wx.EXPAND)

        # Text box that goes below that row of controls.
        self.text_label = basic_label(parent=self, label="Action Text" + ui_helpers.TEXT_FORMATTING_HELP, sizer=left_col_sizer, flag=wx.LEFT)
        self.text_label.SetName("ActionTextLabel:{0:d}".format(self.action_id))
        self.text_text = wx.TextCtrl(parent=self, id=wx.ID_ANY, value="", style=wx.TE_MULTILINE)
        self.text_text.SetMinSize(wx.DLG_UNIT(self.text_text, ui_helpers.MULTILINE_ACTION_TRAIT_DLG_SIZE))
        self.text_text.SetName("ActionText:{0:d}".format(self.action_id))
        left_col_sizer.Add(window=self.text_text, proportion=1, flag=wx.ALL | wx.EXPAND, border=CTRL_BORDER)
        main_row_sizer.Add(sizer=left_col_sizer, proportion=1, flag=wx.EXPAND)

        # Set of checklists for possible keywords to add.
        self.keyword_controls = KeywordControls(container=self.container, parent=self, parent_type="Action", parent_id=self.action_id)
        self.damage_checklist = self.keyword_controls.damage_checklist
        self.source_checklist = self.keyword_controls.source_checklist
        self.type_checklist = self.keyword_controls.type_checklist
        main_row_sizer.Add(sizer=self.keyword_controls)

        # Delete button at the far right of the controls.
        self.delete_button = wx.Button(parent=self, id=wx.ID_ANY, label="X", size=ui_helpers.X_BUTTON_SIZE)
        self.delete_button.SetName("ActionDelete:{0:d}".format(self.action_id))
        self.delete_button.SetFont(ui_helpers.CTRL_FONT)
        self.delete_button.Bind(wx.EVT_BUTTON, self._cb_delete_action)
        main_row_sizer.Add(window=self.delete_button, proportion=0, flag=wx.ALL, border=0)

        self.sizer.Add(sizer=main_row_sizer, flag=wx.EXPAND)

        # Dividing line between this action and the one beneath it.
        self.line = wx.StaticLine(parent=self, id=wx.ID_ANY, style=wx.LI_HORIZONTAL, name="ActionLine:{0:d}".format(self.action_id))
        self.sizer.Add(window=self.line, flag=wx.EXPAND)

        # Initial values, if not added above. Assume a Property by default.
        self.type_box.SetSelection(0)
        self.range_type_box.SetSelection(0)
        self.frequency_box.SetSelection(0)

        self.SetSizerAndFit(self.sizer)

        ActionPanel.next_action_id += 1

    # noinspection PyUnusedLocal
    def _cb_change_frequency(self, event: wx.CommandEvent):
        """
        Callback handling when the allowed frequency of an action changes (e.g., At-Will to Encounter).

        :param event: CommandEvent object containing details about the event.
        """
        self.update_ui()

    # noinspection PyUnusedLocal
    def _cb_delete_action(self, event: wx.CommandEvent):
        """
        Callback handling removing an action from the action list.

        :param event: CommandEvent object containing details about the event.
        """
        self.container.delete_action(self.action_id)

    # noinspection PyUnusedLocal
    def _cb_move_down(self, event: wx.CommandEvent):
        """
        Callback handling moving an action down in the order.

        :param event: CommandEvent object containing details about the event.
        """
        self.container.move_action_down(self.action_id)

    # noinspection PyUnusedLocal
    def _cb_move_up(self, event: wx.CommandEvent):
        """
        Callback handling moving an action up in the order.

        :param event: CommandEvent object containing details about the event.
        """
        self.container.move_action_up(self.action_id)

    def get_keywords(self) -> list:
        """
        Returns a list of the names of all checked keywords.
        Gives sources first, then damages, then types.
        """
        keywords = []
        keywords.extend(self.source_checklist.GetCheckedStrings())
        keywords.extend(self.damage_checklist.GetCheckedStrings())
        keywords.extend(self.type_checklist.GetCheckedStrings())
        return keywords

    def set_id(self, new_id: int):
        """
        Change the ID of this action, cascading to all the component controls.

        :param new_id: Integer ID to change to.
        """
        self.action_id = new_id
        self.up_button.SetName("ActionUp:{0:d}".format(self.action_id))
        self.down_button.SetName("ActionDown:{0:d}".format(self.action_id))
        self.type_box.SetName("ActionType:{0:d}".format(self.action_id))
        self.range_type_box.SetName("ActionRangeType:{0:d}".format(self.action_id))
        self.name_label.SetName("ActionNameLabel:{0:d}".format(self.action_id))
        self.name_text.SetName("ActionName:{0:d}".format(self.action_id))
        self.frequency_box.SetName("ActionFrequency:{0:d}".format(self.action_id))
        self.frequency_text.SetName("ActionFrequencyText:{0:d}".format(self.action_id))
        self.text_label.SetName("ActionTextLabel:{0:d}".format(self.action_id))
        self.text_text.SetName("ActionText:{0:d}".format(self.action_id))
        self.delete_button.SetName("ActionDelete:{0:d}".format(self.action_id))
        self.line.SetName("ActionLine:{0:d}".format(self.action_id))
        self.keyword_controls.set_id(new_id)

    def update_ui(self):
        """
        Adjusts the UI for the action panel.
        """
        # Enable or disable the up/down buttons based on if this panel is already at the top/bottom of the list.
        self.up_button.Enable(self.action_id > min(self.container.actions.keys()))
        self.down_button.Enable(self.action_id < max(self.container.actions.keys()))
        self.Layout()

    def destroy(self):
        """
        Destroys all controls belonging to a action, presumably as part of removing a action from the list.
        """
        self.DestroyChildren()
        self.Destroy()


class TraitPanel(wx.Panel):
    """
    Container class for the controls needed to display an editable trait.
    """
    # ID assigned to the next trait panel created. Auto incremented during panel creation.
    next_trait_id = 0

    def __init__(self, container: 'monster_builder.MonsterBuilderPanel', parent: wx.Window, id: int = wx.ID_ANY, pos: wx.Point = wx.DefaultPosition, size: wx.Size = wx.DefaultSize, style: int = wx.TAB_TRAVERSAL):
        """
        Creates a new set of trait controls in a panel.
        Panel's wx name is always "TraitPanel:X" where X is the trait ID.

        :param container: MonsterBuilderPanel object that will hold the panel.
        :param parent: The owner of the new trait panel.
        :param id: Integer ID of the panel for wx.
        :param pos: Position of the panel.
        :param size: Size of the panel.
        :param style: Style flags for the panel.
        """
        super().__init__(parent=parent, id=id, pos=pos, size=size, style=style, name="TraitPanel:{0:d}".format(self.next_trait_id))
        self.container = container
        self.parent = parent
        self.trait_id = TraitPanel.next_trait_id

        self.sizer = wx.BoxSizer(orient=wx.VERTICAL)

        main_row_sizer = wx.BoxSizer(orient=wx.HORIZONTAL)

        left_col_sizer = wx.BoxSizer(orient=wx.VERTICAL)

        # Buttons to move the trait up and down in the list.
        order_sizer = wx.BoxSizer(orient=wx.VERTICAL)
        self.up_button = wx.Button(parent=self, id=wx.ID_ANY, label="^", size=ui_helpers.UP_DOWN_BUTTON_SIZE)
        self.up_button.SetName("TraitUp:{0:d}".format(self.trait_id))
        self.up_button.SetFont(ui_helpers.CTRL_FONT)
        self.up_button.Bind(wx.EVT_BUTTON, self._cb_move_up)
        order_sizer.Add(window=self.up_button, proportion=0, flag=wx.ALL | wx.ALIGN_LEFT, border=0)
        self.down_button = wx.Button(parent=self, id=wx.ID_ANY, label="v", size=ui_helpers.UP_DOWN_BUTTON_SIZE)
        self.down_button.SetName("TraitDown:{0:d}".format(self.trait_id))
        self.down_button.SetFont(ui_helpers.CTRL_FONT)
        self.down_button.Bind(wx.EVT_BUTTON, self._cb_move_down)
        order_sizer.Add(window=self.down_button, proportion=0, flag=wx.ALL | wx.ALIGN_LEFT, border=0)
        main_row_sizer.Add(sizer=order_sizer, flag=wx.EXPAND)

        # Upper left row of controls.
        upper_row_sizer = wx.BoxSizer(orient=wx.HORIZONTAL)
        self.type_box = basic_dropdown(parent=self, choices=["Property", "Aura"], sizer=upper_row_sizer)
        self.type_box.SetName("TraitType:{0:d}".format(self.trait_id))
        self.type_box.Bind(wx.EVT_CHOICE, self._cb_change_trait_type)
        self.name_label = basic_label(parent=self, label="Name", sizer=upper_row_sizer)
        self.name_label.SetName("TraitNameLabel:{0:d}".format(self.trait_id))
        self.name_text = basic_textbox(parent=self, value="", sizer=upper_row_sizer, minsize=ui_helpers.TEXT_ACTION_TRAIT_NAME_DLG_SIZE)
        self.name_text.SetName("TraitName:{0:d}".format(self.trait_id))
        self.range_label = basic_label(parent=self, label="Range", sizer=upper_row_sizer)
        self.range_label.SetName("TraitRangeLabel:{0:d}".format(self.trait_id))
        self.range = basic_textbox(parent=self, value="0", sizer=upper_row_sizer, minsize=ui_helpers.TEXT_SMALL_DLG_SIZE)
        self.range.SetName("TraitRange:{0:d}".format(self.trait_id))
        left_col_sizer.Add(sizer=upper_row_sizer, flag=wx.EXPAND)

        # Text box that goes below that row of controls.
        self.text_label = basic_label(parent=self, label="Trait Text" + ui_helpers.TEXT_FORMATTING_HELP, sizer=left_col_sizer, flag=wx.LEFT)
        self.text_label.SetName("TraitTextLabel:{0:d}".format(self.trait_id))
        self.text_text = wx.TextCtrl(parent=self, id=wx.ID_ANY, value="", style=wx.TE_MULTILINE)
        self.text_text.SetMinSize(wx.DLG_UNIT(self.text_text, ui_helpers.MULTILINE_ACTION_TRAIT_DLG_SIZE))
        self.text_text.SetName("TraitText:{0:d}".format(self.trait_id))
        left_col_sizer.Add(window=self.text_text, proportion=1, flag=wx.ALL | wx.EXPAND, border=CTRL_BORDER)
        main_row_sizer.Add(sizer=left_col_sizer, proportion=1, flag=wx.EXPAND)

        # Set of checklists for possible keywords to add.
        self.keyword_controls = KeywordControls(container=self.container, parent=self, parent_type="Trait", parent_id=self.trait_id)
        self.damage_checklist = self.keyword_controls.damage_checklist
        self.source_checklist = self.keyword_controls.source_checklist
        self.type_checklist = self.keyword_controls.type_checklist
        main_row_sizer.Add(sizer=self.keyword_controls)

        # Delete button at the far right of the controls.
        self.delete_button = wx.Button(parent=self, id=wx.ID_ANY, label="X", size=ui_helpers.X_BUTTON_SIZE)
        self.delete_button.SetName("TraitDelete:{0:d}".format(self.trait_id))
        self.delete_button.SetFont(ui_helpers.CTRL_FONT)
        self.delete_button.Bind(wx.EVT_BUTTON, self._cb_delete_trait)
        main_row_sizer.Add(window=self.delete_button, proportion=0, flag=wx.ALL, border=0)

        self.sizer.Add(sizer=main_row_sizer, flag=wx.EXPAND)

        # Dividing line between this trait and the one beneath it.
        self.line = wx.StaticLine(parent=self, id=wx.ID_ANY, style=wx.LI_HORIZONTAL, name="TraitLine:{0:d}".format(self.trait_id))
        self.sizer.Add(window=self.line, flag=wx.EXPAND)

        # Initial values, if not added above. Assume a Property by default.
        self.type_box.SetSelection(0)
        self.range.Disable()

        self.SetSizerAndFit(self.sizer)

        TraitPanel.next_trait_id += 1

    # noinspection PyUnusedLocal
    def _cb_change_trait_type(self, event):
        """
        Callback handling when the type of a trait changes.
        Disables or enables the range of the trait depending on if its a property or aura, respectively.

        :param event: CommandEvent object containing details about the event.
        """
        self.update_ui()

    # noinspection PyUnusedLocal
    def _cb_delete_trait(self, event):
        """
        Callback handling removing a trait from the trait list.

        :param event: CommandEvent object containing details about the event.
        """
        self.container.delete_trait(self.trait_id)

    # noinspection PyUnusedLocal
    def _cb_move_down(self, event: wx.CommandEvent):
        """
        Callback handling moving a trait down in the order.

        :param event: CommandEvent object containing details about the event.
        """
        self.container.move_trait_down(self.trait_id)

    # noinspection PyUnusedLocal
    def _cb_move_up(self, event: wx.CommandEvent):
        """
        Callback handling moving a trait up in the order.

        :param event: CommandEvent object containing details about the event.
        """
        self.container.move_trait_up(self.trait_id)

    def get_keywords(self):
        """
        Returns a list of the names of all checked keywords.
        Gives sources first, then damages, then types.
        """
        keywords = []
        keywords.extend(self.source_checklist.GetCheckedStrings())
        keywords.extend(self.damage_checklist.GetCheckedStrings())
        keywords.extend(self.type_checklist.GetCheckedStrings())
        return keywords

    def set_id(self, new_id: int):
        """
        Change the ID of this trait, cascading to all the component controls.

        :param new_id: Integer ID to change to.
        """
        self.trait_id = new_id
        self.up_button.SetName("TraitUp:{0:d}".format(self.trait_id))
        self.down_button.SetName("TraitDown:{0:d}".format(self.trait_id))
        self.type_box.SetName("TraitType:{0:d}".format(self.trait_id))
        self.name_label.SetName("TraitNameLabel:{0:d}".format(self.trait_id))
        self.name_text.SetName("TraitName:{0:d}".format(self.trait_id))
        self.range_label.SetName("TraitRangeLabel:{0:d}".format(self.trait_id))
        self.range.SetName("TraitRange:{0:d}".format(self.trait_id))
        self.text_label.SetName("TraitTextLabel:{0:d}".format(self.trait_id))
        self.text_text.SetName("TraitText:{0:d}".format(self.trait_id))
        self.delete_button.SetName("TraitDelete:{0:d}".format(self.trait_id))
        self.line.SetName("TraitLine:{0:d}".format(self.trait_id))
        self.keyword_controls.set_id(new_id)

    def update_ui(self):
        """
        Adjusts the UI for a trait panel based on what kind of trait parameters are selected.
        """
        trait_type = self.type_box.GetStringSelection().lower()
        if trait_type == "aura":
            self.range.Enable()
            self.type_checklist.Check(self.type_checklist.FindString("Aura"), True)
        elif trait_type == "property":
            self.range.Disable()
            self.type_checklist.Check(self.type_checklist.FindString("Aura"), False)
        # Enable or disable the up/down buttons based on if this panel is already at the top/bottom of the list.
        self.up_button.Enable(self.trait_id > min(self.container.traits.keys()))
        self.down_button.Enable(self.trait_id < max(self.container.traits.keys()))
        self.Layout()

    def destroy(self):
        """
        Destroys all controls belonging to a trait, presumably as part of removing a trait from the list.
        """
        self.DestroyChildren()
        self.Destroy()
