# -*- coding: utf-8 -*-
"""
Contains definition of the monster builder window.

If run as the main script, will create and display a window allowing the user
to edit, save, and export a single monster to a file.
"""
import collections
# noinspection PyUnresolvedReferences
import math
import os
import traceback
import types
# noinspection PyUnresolvedReferences
import wx

from __init__ import __version__
import exporters
import file_utils
from monster_ui_helpers import *
# noinspection PyUnresolvedReferences
import stat_stuff
from stat_stuff import LAND_SPEED_KEY
import ui_helpers
from ui_helpers import *

# ----------
# Main monster building panel.
# ----------


class MonsterBuilderPanel(wx.Panel):
    """
    Main panel for creating/editing a monster.
    """
    BLOODIED_STR = "/ {0:d} Bloodied"  # String to use with format() to display a monster's Bloodied HP.
    DEFAULT_VAL_STR = " ({0:d})"  # String to use with format() to display a suggested/calculated value on the UI.

    def __init__(self, frame: ui_helpers.BuilderFrame, parent: wx.Window):
        """
        Constructor for the window. Sets up and initializes UI elements.

        :param frame: MonsterBuilderFrame or EncounterBuilderFrame object that will hold this panel. Used to reference frame controls like the status bar.
        :param parent: The owner of this panel.
        """
        super().__init__(parent=parent, id=wx.ID_ANY)
        self.frame = frame

        self.last_save_file = ''
        # Control containers. Populated with _setup_ui().
        self.notebook = None
        self.panels = types.SimpleNamespace()
        self.static_boxes = types.SimpleNamespace()
        # Controls. Populated with _setup_ui().
        self.buttons = types.SimpleNamespace()
        self.checkboxes = types.SimpleNamespace()
        self.checklists = types.SimpleNamespace()
        self.dropdowns = types.SimpleNamespace()
        self.labels = types.SimpleNamespace()
        self.lines = types.SimpleNamespace()
        self.textboxes = types.SimpleNamespace()
        self.sizers = types.SimpleNamespace()
        # Control collections. Populated with _setup_ui().
        self.traits = {}  # Key: Trait ID, Value: TraitControls object.
        self.actions = {}  # Key: Action ID, Value: ActionControls object.
        self.skills = collections.OrderedDict()  # Key: Skill name, Value: SkillControls object.
        self.senses = {}  # Key: Sense name, Value: SenseControls object.
        self.speeds = {}  # Key: Speeds checklist index, Value: SimpleNamespace with attributes: sizer, label, textbox, mods.
        self.resist_vuln_opts = {}  # Key: Resist/Vuln/Immune name, Value: ResistVulnControls object.
        # Guards to ensure looping updates don't happen.
        self.laying_out = False
        self.recalculating = False
        self.repainting = False
        self.resizing = False
        self.updating_ui = False

        self._setup_ui()
        self._initialize()

    def _setup_ui(self):
        """
        Sets up and places all UI elements on the window.
        """
        # Stop the UI from updating while everything is loading in.
        self.Freeze()
        # Font setup. This should cascade to all children of this panel.
        self.SetFont(ui_helpers.TEXT_FONT)

        self.Hide()

        self.SetMinSize(wx.Size(1030, 420))

        # Notebook to hold each of the panels.
        self.notebook = wx.Notebook(parent=self, id=wx.ID_ANY, style=wx.CLIP_CHILDREN)

        # Main stats panel, holding all the common fields.
        # ==========
        self.panels.stats = wx.Panel(parent=self.notebook, id=wx.ID_ANY)
        stats_sizer = wx.BoxSizer(orient=wx.VERTICAL)

        # Name, role, etc.
        # ----------
        name_etc_sizer = wx.BoxSizer(orient=wx.HORIZONTAL)

        self.labels.name = basic_label(parent=self.panels.stats, label="Name:", sizer=name_etc_sizer, minsize=ui_helpers.LABEL_SMALL_DLG_SIZE, style=wx.ALIGN_RIGHT)
        self.textboxes.name = basic_textbox(parent=self.panels.stats, value="", sizer=name_etc_sizer, minsize=ui_helpers.TEXT_LARGE_DLG_SIZE, proportion=5)
        self.labels.level = basic_label(parent=self.panels.stats, label="Lvl", sizer=name_etc_sizer, minsize=ui_helpers.LABEL_SMALL_DLG_SIZE, style=wx.ALIGN_RIGHT)
        self.dropdowns.level = basic_dropdown(parent=self.panels.stats, choices=[str(i) for i in range(1, 31)], sizer=name_etc_sizer, minsize=ui_helpers.DROPDOWN_SMALL_DLG_SIZE)
        self.dropdowns.level.Bind(wx.EVT_CHOICE, self._cb_level_change)
        self.dropdowns.role_mod = basic_dropdown(parent=self.panels.stats, choices=[role_mod for role_mod in stat_stuff.ROLE_MODS], sizer=name_etc_sizer, minsize=ui_helpers.DROPDOWN_MED_DLG_SIZE)
        self.dropdowns.role_mod.Bind(wx.EVT_CHOICE, self._cb_role_mod_change)
        self.dropdowns.role = basic_dropdown(parent=self.panels.stats, choices=[role for role in stat_stuff.ROLES], sizer=name_etc_sizer, minsize=ui_helpers.DROPDOWN_MED_DLG_SIZE)
        self.dropdowns.role.Bind(wx.EVT_CHOICE, self._cb_role_change)
        self.labels.xp = basic_label(parent=self.panels.stats, label="XP" + self.DEFAULT_VAL_STR.format(0), sizer=name_etc_sizer, minsize=ui_helpers.LABEL_MED_DLG_SIZE, style=wx.ALIGN_RIGHT)
        self.textboxes.xp = basic_textbox(parent=self.panels.stats, value="", sizer=name_etc_sizer, minsize=ui_helpers.TEXT_SMALL_DLG_SIZE)
        self.checkboxes.custom_xp = basic_checkbox(parent=self.panels.stats, label="Custom", sizer=name_etc_sizer, minsize=ui_helpers.CHECK_SMALL_DLG_SIZE)
        self.checkboxes.custom_xp.Bind(wx.EVT_CHECKBOX, self._cb_custom_xp)

        stats_sizer.Add(sizer=name_etc_sizer, flag=wx.EXPAND)

        # Size, origin, Init, & Perception
        # ----------
        type_etc_sizer = wx.BoxSizer(orient=wx.HORIZONTAL)

        self.dropdowns.size = basic_dropdown(parent=self.panels.stats, choices=[size for size in stat_stuff.SIZES], sizer=type_etc_sizer, minsize=ui_helpers.DROPDOWN_MED_DLG_SIZE)
        self.dropdowns.origin = basic_dropdown(parent=self.panels.stats, choices=[origin for origin in stat_stuff.ORIGINS], sizer=type_etc_sizer, minsize=ui_helpers.DROPDOWN_MED_DLG_SIZE)
        self.dropdowns.type = basic_dropdown(parent=self.panels.stats, choices=[creature_type for creature_type in stat_stuff.TYPES], sizer=type_etc_sizer, minsize=ui_helpers.DROPDOWN_MED_DLG_SIZE)
        self.checkboxes.swarm = basic_checkbox(parent=self.panels.stats, label="Swarm", sizer=type_etc_sizer, minsize=ui_helpers.CHECK_SMALL_DLG_SIZE)
        self.checkboxes.swarm.Bind(wx.EVT_CHECKBOX, self._cb_swarm_select)
        self.labels.swarm_vuln, self.textboxes.swarm_vuln = ui_helpers.labeled_textbox(parent=self.panels.stats, label="Area/Close Damage Vulnerability", value="", sizer=type_etc_sizer, text_minsize=ui_helpers.TEXT_SMALL_DLG_SIZE)
        type_etc_sizer.AddStretchSpacer(1)
        self.labels.init = basic_label(parent=self.panels.stats, label="Init" + self.DEFAULT_VAL_STR.format(0), sizer=type_etc_sizer, minsize=ui_helpers.LABEL_STAT_DLG_SIZE, flag=wx.ALL)
        self.textboxes.init = basic_textbox(parent=self.panels.stats, value="", sizer=type_etc_sizer, minsize=ui_helpers.TEXT_SMALL_DLG_SIZE)
        self.checkboxes.custom_init = basic_checkbox(parent=self.panels.stats, label="Custom", sizer=type_etc_sizer, minsize=ui_helpers.CHECK_SMALL_DLG_SIZE)
        self.checkboxes.custom_init.Bind(wx.EVT_CHECKBOX, self._cb_custom_init)
        self.labels.perception = basic_label(parent=self.panels.stats, label="Perception", sizer=type_etc_sizer, minsize=ui_helpers.LABEL_STAT_DLG_SIZE, flag=wx.ALL)
        self.textboxes.perception = basic_textbox(parent=self.panels.stats, value="", sizer=type_etc_sizer, minsize=ui_helpers.TEXT_SMALL_DLG_SIZE)
        # Permanently disable the perception text box, since it's just echoing what's in the skill list.
        self.textboxes.perception.Disable()

        stats_sizer.Add(sizer=type_etc_sizer, flag=wx.EXPAND)

        # HP, defenses.
        # ----------
        hp_def_sizer = wx.BoxSizer(orient=wx.HORIZONTAL)

        self.labels.hp = basic_label(parent=self.panels.stats, label="HP" + self.DEFAULT_VAL_STR.format(0), sizer=hp_def_sizer, minsize=ui_helpers.LABEL_STAT_DLG_SIZE)
        self.textboxes.hp = basic_textbox(parent=self.panels.stats, value="", sizer=hp_def_sizer, minsize=ui_helpers.TEXT_SMALL_DLG_SIZE)
        self.textboxes.hp.Bind(wx.EVT_TEXT, self._cb_hp_change)
        self.labels.bloodied = basic_label(parent=self.panels.stats, label=self.BLOODIED_STR.format(0), sizer=hp_def_sizer, minsize=ui_helpers.LABEL_MED_DLG_SIZE)

        hp_def_sizer.AddStretchSpacer(1)

        self.labels.ac = basic_label(parent=self.panels.stats, label="AC" + self.DEFAULT_VAL_STR.format(0), sizer=hp_def_sizer, minsize=ui_helpers.LABEL_STAT_DLG_SIZE, flag=wx.ALL)
        self.textboxes.ac = basic_textbox(parent=self.panels.stats, value="", sizer=hp_def_sizer, minsize=ui_helpers.TEXT_SMALL_DLG_SIZE)
        self.labels.fort = basic_label(parent=self.panels.stats, label="Fort" + self.DEFAULT_VAL_STR.format(0), sizer=hp_def_sizer, minsize=ui_helpers.LABEL_STAT_DLG_SIZE, flag=wx.ALL)
        self.textboxes.fort = basic_textbox(parent=self.panels.stats, value="", sizer=hp_def_sizer, minsize=ui_helpers.TEXT_SMALL_DLG_SIZE)
        self.labels.ref = basic_label(parent=self.panels.stats, label="Ref" + self.DEFAULT_VAL_STR.format(0), sizer=hp_def_sizer, minsize=ui_helpers.LABEL_STAT_DLG_SIZE, flag=wx.ALL)
        self.textboxes.ref = basic_textbox(parent=self.panels.stats, value="", sizer=hp_def_sizer, minsize=ui_helpers.TEXT_SMALL_DLG_SIZE)
        self.labels.will = basic_label(parent=self.panels.stats, label="Will" + self.DEFAULT_VAL_STR.format(0), sizer=hp_def_sizer, minsize=ui_helpers.LABEL_STAT_DLG_SIZE, flag=wx.ALL)
        self.textboxes.will = basic_textbox(parent=self.panels.stats, value="", sizer=hp_def_sizer, minsize=ui_helpers.TEXT_SMALL_DLG_SIZE)

        stats_sizer.Add(sizer=hp_def_sizer, flag=wx.EXPAND)

        # Str/Con/Dex/Int/Wis/Cha.
        # ----------
        attributes_block_sizer = wx.BoxSizer(orient=wx.VERTICAL)

        attributes_template_sizer = wx.BoxSizer(orient=wx.HORIZONTAL)
        self.labels.attribute_role = basic_label(parent=self.panels.stats, label="Attribute Template:", sizer=attributes_template_sizer, minsize=ui_helpers.LABEL_MED_DLG_SIZE)
        self.dropdowns.attribute_role = basic_dropdown(parent=self.panels.stats, choices=list(stat_stuff.DEFAULT_ROLE_STATS.keys()), sizer=attributes_template_sizer, minsize=ui_helpers.DROPDOWN_MED_DLG_SIZE)
        self.dropdowns.attribute_role.Bind(wx.EVT_CHOICE, self._cb_attribute_role_change)
        self.checkboxes.custom_attributes = basic_checkbox(parent=self.panels.stats, label="Custom", sizer=attributes_template_sizer, minsize=ui_helpers.CHECK_SMALL_DLG_SIZE)
        self.checkboxes.custom_attributes.Bind(wx.EVT_CHECKBOX, self._cb_attribute_custom)
        attributes_block_sizer.Add(sizer=attributes_template_sizer, flag=wx.EXPAND)

        attributes_sizer = wx.BoxSizer(orient=wx.HORIZONTAL)
        self.labels.str = basic_label(parent=self.panels.stats, label="Str", sizer=attributes_sizer, minsize=ui_helpers.LABEL_STAT_DLG_SIZE, flag=wx.ALL)
        self.textboxes.str = basic_textbox(parent=self.panels.stats, value="", sizer=attributes_sizer, minsize=ui_helpers.TEXT_SMALL_DLG_SIZE)
        self.textboxes.str.Bind(wx.EVT_TEXT, self._cb_attribute_change)
        self.labels.con = basic_label(parent=self.panels.stats, label="Con", sizer=attributes_sizer, minsize=ui_helpers.LABEL_STAT_DLG_SIZE, flag=wx.ALL)
        self.textboxes.con = basic_textbox(parent=self.panels.stats, value="", sizer=attributes_sizer, minsize=ui_helpers.TEXT_SMALL_DLG_SIZE)
        self.textboxes.con.Bind(wx.EVT_TEXT, self._cb_attribute_change)
        self.labels.dex = basic_label(parent=self.panels.stats, label="Dex", sizer=attributes_sizer, minsize=ui_helpers.LABEL_STAT_DLG_SIZE, flag=wx.ALL)
        self.textboxes.dex = basic_textbox(parent=self.panels.stats, value="", sizer=attributes_sizer, minsize=ui_helpers.TEXT_SMALL_DLG_SIZE)
        self.textboxes.dex.Bind(wx.EVT_TEXT, self._cb_attribute_change)
        self.labels.int = basic_label(parent=self.panels.stats, label="Int", sizer=attributes_sizer, minsize=ui_helpers.LABEL_STAT_DLG_SIZE, flag=wx.ALL)
        self.textboxes.int = basic_textbox(parent=self.panels.stats, value="", sizer=attributes_sizer, minsize=ui_helpers.TEXT_SMALL_DLG_SIZE)
        self.textboxes.int.Bind(wx.EVT_TEXT, self._cb_attribute_change)
        self.labels.wis = basic_label(parent=self.panels.stats, label="Wis", sizer=attributes_sizer, minsize=ui_helpers.LABEL_STAT_DLG_SIZE, flag=wx.ALL)
        self.textboxes.wis = basic_textbox(parent=self.panels.stats, value="", sizer=attributes_sizer, minsize=ui_helpers.TEXT_SMALL_DLG_SIZE)
        self.textboxes.wis.Bind(wx.EVT_TEXT, self._cb_attribute_change)
        self.labels.cha = basic_label(parent=self.panels.stats, label="Cha", sizer=attributes_sizer, minsize=ui_helpers.LABEL_STAT_DLG_SIZE, flag=wx.ALL)
        self.textboxes.cha = basic_textbox(parent=self.panels.stats, value="", sizer=attributes_sizer, minsize=ui_helpers.TEXT_SMALL_DLG_SIZE)
        self.textboxes.cha.Bind(wx.EVT_TEXT, self._cb_attribute_change)
        attributes_block_sizer.Add(sizer=attributes_sizer, flag=wx.EXPAND)

        stats_sizer.Add(sizer=attributes_block_sizer, flag=wx.EXPAND)

        # Languages and Equipment.
        # ----------
        lang_sizer = wx.BoxSizer(orient=wx.HORIZONTAL)
        self.labels.languages, self.textboxes.languages = ui_helpers.labeled_textbox(parent=self.panels.stats, label="Languages", value="", sizer=lang_sizer, text_minsize=ui_helpers.TEXT_LARGE_DLG_SIZE)
        stats_sizer.Add(lang_sizer, flag=wx.EXPAND)

        equip_sizer = wx.BoxSizer(orient=wx.HORIZONTAL)
        self.labels.equipment, self.textboxes.equipment = ui_helpers.labeled_textbox(parent=self.panels.stats, label="Equipment", value="", sizer=equip_sizer, text_minsize=ui_helpers.TEXT_LARGE_DLG_SIZE)
        stats_sizer.Add(equip_sizer, flag=wx.EXPAND)

        # Alignment.
        # ----------
        alignment_sizer = wx.BoxSizer(orient=wx.HORIZONTAL)
        self.labels.alignment = ui_helpers.basic_label(parent=self.panels.stats, label="Alignment", sizer=alignment_sizer)
        self.dropdowns.alignment = basic_editable_dropdown(parent=self.panels.stats, choices=[alignment for alignment in stat_stuff.ALIGNMENTS], value="", sizer=alignment_sizer, style=wx.CB_DROPDOWN)

        stats_sizer.Add(alignment_sizer, flag=wx.EXPAND)

        # Optional labels showing stat boosts & alterations for role mods.
        # ----------
        self.labels.role_mod_info = basic_label(parent=self.panels.stats, label="", sizer=stats_sizer)
        self.labels.swarm_info = basic_label(parent=self.panels.stats, label=ui_helpers.SWARM_MOD_HELP, sizer=stats_sizer)

        # Main stats panel sizer & placement setup.
        # ----------
        self.panels.stats.SetSizer(stats_sizer)

        # Properties panel, holds all the skills, senses, speeds, and resistances panels.
        # ==========
        self.panels.properties = wx.Panel(parent=self.notebook, id=wx.ID_ANY)

        # Skills panel.
        # ==========
        self.panels.skills = SkillsPanel(container=self, parent=self.panels.properties)

        # Senses panel.
        # ==========
        # Set up senses.
        self.panels.senses = wx.Panel(parent=self.panels.properties, id=wx.ID_ANY)
        self.sizers.senses_sizer = wx.StaticBoxSizer(orient=wx.HORIZONTAL, parent=self.panels.senses, label="Senses")
        self.static_boxes.senses_box = self.sizers.senses_sizer.GetStaticBox()

        # Scrolled window for senses so if they get squished by other elements then they can still be displayed.
        self.panels.sense_scroll = wx.ScrolledWindow(self.panels.senses, wx.ID_ANY, style=wx.VSCROLL, name="Senses:List")
        self.panels.sense_scroll.SetScrollRate(wx.DefaultCoord, ui_helpers.SCROLL_HEIGHT)
        self.panels.sense_scroll.SetMinSize(ui_helpers.SENSES_WINDOW_SIZE)
        self.panels.sense_scroll.SetVirtualSize(ui_helpers.SENSES_WINDOW_SIZE)
        sense_scroll_sizer = wx.BoxSizer(orient=wx.VERTICAL)
        for sense in sorted(stat_stuff.SENSES.keys()):
            new_sense = SenseControls(label=sense, has_value=stat_stuff.SENSES[sense], container=self, parent=self.panels.sense_scroll)
            self.senses[new_sense.label] = new_sense
            sense_scroll_sizer.Add(new_sense, flag=wx.EXPAND)
        self.panels.sense_scroll.SetSizer(sense_scroll_sizer)
        self.sizers.senses_sizer.Add(self.panels.sense_scroll, flag=wx.EXPAND)

        # Senses panel sizer & placement setup.
        self.panels.senses.SetSizer(self.sizers.senses_sizer)

        # Speeds panel.
        # ==========
        # Set up speeds.
        self.panels.speeds = wx.Panel(parent=self.panels.properties, id=wx.ID_ANY)
        self.sizers.speeds_sizer = wx.StaticBoxSizer(orient=wx.HORIZONTAL, parent=self.panels.speeds, label="Speeds")
        self.static_boxes.speeds_box = self.sizers.speeds_sizer.GetStaticBox()

        speed_types_sizer = wx.BoxSizer(wx.VERTICAL)
        self.checklists.speeds = basic_checklist(parent=self.static_boxes.speeds_box, choices=stat_stuff.SPEEDS, sizer=speed_types_sizer, minsize=ui_helpers.CHECKLIST_MED_DLG_SIZE)
        self.checklists.speeds.Bind(event=wx.EVT_CHECKLISTBOX, handler=self._cb_speed_clicked)
        self.checkboxes.phasing = basic_checkbox(parent=self.static_boxes.speeds_box, label="Phasing", sizer=speed_types_sizer, minsize=ui_helpers.CHECK_MED_DLG_SIZE)
        self.sizers.speeds_sizer.Add(speed_types_sizer, flag=wx.EXPAND)

        self.sizers.speed_values_sizer = wx.BoxSizer(wx.VERTICAL)
        # Add the base "land speed" one that's not part of the checklist.
        speed_value_sizer = wx.BoxSizer(orient=wx.HORIZONTAL)
        label, textbox = labeled_textbox(parent=self.static_boxes.speeds_box, label="Speed", value="", sizer=speed_value_sizer, text_minsize=ui_helpers.TEXT_SMALL_DLG_SIZE, label_border=1, text_border=1)
        self.speeds[LAND_SPEED_KEY] = types.SimpleNamespace()
        self.speeds[LAND_SPEED_KEY].sizer = speed_value_sizer
        self.speeds[LAND_SPEED_KEY].label = label
        self.speeds[LAND_SPEED_KEY].textbox = textbox
        self.sizers.speed_values_sizer.Add(speed_value_sizer, flag=wx.EXPAND)
        # Add in labeled textboxes for each special speed, but hide them initially.
        for i in range(0, self.checklists.speeds.GetCount()):
            speed_value_sizer = wx.BoxSizer(orient=wx.HORIZONTAL)
            label, textbox = labeled_textbox(parent=self.static_boxes.speeds_box, label=stat_stuff.SPEEDS[i], value="", sizer=speed_value_sizer, text_minsize=ui_helpers.TEXT_SMALL_DLG_SIZE, label_border=1, text_border=1)
            self.speeds[i] = types.SimpleNamespace()
            self.speeds[i].sizer = speed_value_sizer
            self.speeds[i].label = label
            self.speeds[i].textbox = textbox
            # Include the mods for any speed types that have them.
            self.speeds[i].mods = []
            for j in range(0, len(stat_stuff.SPEED_MODS[i])):
                self.speeds[i].mods.append(basic_checkbox(parent=self.static_boxes.speeds_box, label=stat_stuff.SPEED_MODS[i][j], sizer=speed_value_sizer, border=1))
            self.sizers.speed_values_sizer.Add(speed_value_sizer, flag=wx.EXPAND)
            # Hide all the special speed stuff initially.
            self.speeds[i].label.Hide()
            self.speeds[i].textbox.Hide()
            for mod in self.speeds[i].mods:
                mod.Hide()
                self.sizers.speed_values_sizer.Hide(mod)
            self.sizers.speed_values_sizer.Hide(self.speeds[i].label)
            self.sizers.speed_values_sizer.Hide(self.speeds[i].textbox)

        self.sizers.speeds_sizer.Add(self.sizers.speed_values_sizer, flag=wx.EXPAND)

        # Speeds panel sizer & placement setup.
        self.panels.speeds.SetSizer(self.sizers.speeds_sizer)

        # Resistances, Vulnerabilities, Immunities.
        # ==========
        self.panels.resist_vuln_imm = wx.Panel(parent=self.panels.properties, id=wx.ID_ANY)
        self.sizers.resist_vuln_imm_sizer = wx.StaticBoxSizer(orient=wx.HORIZONTAL, parent=self.panels.resist_vuln_imm, label="Resistances, Vulnerabilities, Immunities")
        self.static_boxes.resist_vuln_imm_box = self.sizers.resist_vuln_imm_sizer.GetStaticBox()

        # Add specific options for each resistance/vuln.
        self.panels.rv = wx.ScrolledWindow(self.panels.resist_vuln_imm, wx.ID_ANY, style=wx.VSCROLL, name="ResistVuln:List")
        self.panels.rv.SetScrollRate(wx.DefaultCoord, ui_helpers.SCROLL_HEIGHT)
        self.panels.rv.SetVirtualSize(ui_helpers.RESIST_VULN_WINDOW_SIZE)
        rv_opts_sizer = wx.BoxSizer(orient=wx.VERTICAL)
        for rv in stat_stuff.RESIST_VULNS:
            new_opts = ResistVulnControls(container=self, parent=self.panels.rv, label=rv, value="", has_rv=True)
            self.resist_vuln_opts[rv] = new_opts
            rv_opts_sizer.Add(new_opts, flag=wx.EXPAND)
            # Hide the extra keyword options initially.
            new_opts.hide()
        self.panels.rv.SetSizer(rv_opts_sizer)
        self.sizers.resist_vuln_imm_sizer.Add(self.panels.rv, flag=wx.EXPAND)

        # Add specific options for each immunity.
        self.panels.imm = wx.ScrolledWindow(self.panels.resist_vuln_imm, wx.ID_ANY, style=wx.VSCROLL, name="Imm:List")
        self.panels.imm.SetScrollRate(wx.DefaultCoord, 10)
        self.panels.imm.SetVirtualSize(ui_helpers.IMMUNE_WINDOW_SIZE)
        imm_opts_sizer = wx.BoxSizer(orient=wx.VERTICAL)
        for imm in stat_stuff.IMMUNITIES:
            new_opts = ResistVulnControls(container=self, parent=self.panels.imm, label=imm, value="", has_rv=False)
            self.resist_vuln_opts[imm] = new_opts
            imm_opts_sizer.Add(new_opts, flag=wx.EXPAND)
            # Hide the extra keyword options initially.
            new_opts.hide()
        self.panels.imm.SetSizer(imm_opts_sizer)
        self.sizers.resist_vuln_imm_sizer.Add(self.panels.imm, flag=wx.EXPAND)

        # Resistance, vulnerability, and immunity panel sizer & placement setup.
        self.panels.resist_vuln_imm.SetSizer(self.sizers.resist_vuln_imm_sizer)

        # Dummy stretch space on the right edge of the top row.
        # ==========
        dummy_panel = wx.Panel(parent=self.panels.properties, id=wx.ID_ANY)
        dummy_sizer = wx.BoxSizer(orient=wx.VERTICAL)
        dummy_sizer.AddStretchSpacer(1)
        dummy_panel.SetSizer(dummy_sizer)

        # Traits (Auras, Properties, etc.) and Actions (Attacks, movement, reactions/interrupts, etc.).
        # These go on their own notebook page.
        # ==========
        self.panels.traits_actions = wx.Panel(parent=self.notebook, id=wx.ID_ANY)
        self.sizers.traits_actions_sizer = wx.StaticBoxSizer(orient=wx.VERTICAL, parent=self.panels.traits_actions, label="Traits and Actions")
        self.static_boxes.traits_actions_box = self.sizers.traits_actions_sizer.GetStaticBox()

        # Controls to add traits & actions.
        ta_controls_sizer = wx.BoxSizer(orient=wx.HORIZONTAL)

        self.buttons.add_trait = basic_button(parent=self.static_boxes.traits_actions_box, label="Add Trait", sizer=ta_controls_sizer)
        self.buttons.add_trait.Bind(wx.EVT_BUTTON, self._cb_add_trait)

        self.buttons.add_action = basic_button(parent=self.static_boxes.traits_actions_box, label="Add Action", sizer=ta_controls_sizer)
        self.buttons.add_action.Bind(wx.EVT_BUTTON, self._cb_add_action)

        self.buttons.add_swarm_aura = basic_button(parent=self.static_boxes.traits_actions_box, label="Add Swarm Aura Template", sizer=ta_controls_sizer)
        self.buttons.add_swarm_aura.Bind(wx.EVT_BUTTON, self._cb_add_swarm_aura)

        self.sizers.traits_actions_sizer.Add(ta_controls_sizer, flag=wx.EXPAND)

        self.sizers.traits_actions_sizer.Add(window=wx.StaticLine(parent=self.static_boxes.traits_actions_box, id=wx.ID_ANY, style=wx.LI_HORIZONTAL), flag=wx.EXPAND)

        # Scrollable panel to hold the traits & actions themselves.
        self.panels.ta_list = wx.ScrolledWindow(parent=self.static_boxes.traits_actions_box, id=wx.ID_ANY)
        self.panels.ta_list.SetScrollRate(wx.DefaultCoord, ui_helpers.SCROLL_HEIGHT)
        self.panels.ta_list.SetMinSize(ui_helpers.TRAIT_ACTION_WINDOW_SIZE)
        self.sizers.ta_list_sizer = wx.BoxSizer(orient=wx.VERTICAL)

        # A line to divide traits from actions.
        self.lines.ta_line = wx.Panel(parent=self.panels.ta_list, id=wx.ID_ANY, size=wx.Size(wx.DefaultCoord, 10))
        self.lines.ta_line.SetBackgroundColour(wx.Colour(0, 0, 0))
        self.sizers.ta_list_sizer.Add(self.lines.ta_line, flag=wx.TOP | wx.BOTTOM | wx.EXPAND, border=CTRL_BORDER)
        self.sizers.ta_list_sizer.Hide(self.lines.ta_line)

        self.panels.ta_list.SetSizer(self.sizers.ta_list_sizer)
        self._organize_traits_actions()

        self.sizers.traits_actions_sizer.Add(self.panels.ta_list, flag=wx.EXPAND, border=0)

        # Trait & action panel sizer & placement setup.
        self.panels.traits_actions.SetSizer(self.sizers.traits_actions_sizer)

        # Final window sizer setup.
        # ==========
        window_sizer = wx.BoxSizer(orient=wx.VERTICAL)
        first_row_sizer = wx.BoxSizer(orient=wx.HORIZONTAL)
        second_row_sizer = wx.BoxSizer(orient=wx.HORIZONTAL)

        self.notebook.InsertPage(index=0, page=self.panels.stats, text="Basic Stats", select=True)

        first_row_sizer.Add(self.panels.skills, proportion=0, flag=wx.EXPAND)
        first_row_sizer.Add(self.panels.speeds, proportion=0, flag=wx.EXPAND)
        first_row_sizer.Add(dummy_panel, proportion=1, flag=wx.EXPAND)
        second_row_sizer.Add(self.panels.senses, proportion=0, flag=wx.EXPAND)
        second_row_sizer.Add(self.panels.resist_vuln_imm, proportion=0, flag=wx.EXPAND)
        self.sizers.properties = wx.BoxSizer(orient=wx.VERTICAL)
        self.sizers.properties.Add(first_row_sizer, proportion=0, flag=wx.EXPAND)
        self.sizers.properties.Add(second_row_sizer, proportion=0, flag=wx.EXPAND)
        self.panels.properties.SetSizer(self.sizers.properties)
        self.notebook.InsertPage(index=1, page=self.panels.properties, text="Skills, Speeds, Senses, Resistances", select=False)

        self.notebook.InsertPage(index=2, page=self.panels.traits_actions, text="Traits and Actions", select=False)

        window_sizer.Add(self.notebook)
        self.SetSizer(window_sizer)
        self.Thaw()

    def _initialize(self):
        """
        Gives UI elements their initial values.
        """
        self.reset()

        self.repaint()

        self.Show()

    # Callbacks
    # ---------

    # noinspection PyUnusedLocal
    def _cb_add_action(self, event: wx.CommandEvent):
        """
        Callback handling adding a new action to the action list.

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

    # noinspection PyUnusedLocal
    def _cb_add_swarm_aura(self, event: wx.CommandEvent):
        """
        Callback handling adding a template aura "attack" for a swarm to the trait list.

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

    # noinspection PyUnusedLocal
    def _cb_add_trait(self, event: wx.CommandEvent):
        """
        Callback handling adding a new trait to the trait list.

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

    # noinspection PyUnusedLocal
    def _cb_attribute_change(self, event: wx.CommandEvent):
        """
        Callback handling when one of the monster's attributes changes.

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

    def _cb_attribute_custom(self, event: wx.CommandEvent):
        """
        Callback handling when the user (un)checks the option to use custom attribute scores.

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

    def _cb_attribute_role_change(self, event: wx.CommandEvent):
        """
        Callback handling when the monster's "attribute role" template changes.

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

    def _cb_custom_init(self, event: wx.CommandEvent):
        """
        Callback handling when the user (un)checks the option to use a custom Initiative value.

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

    def _cb_custom_xp(self, event: wx.CommandEvent):
        """
        Callback handling when the user (un)checks the option to use a custom XP value.

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

    # noinspection PyUnusedLocal
    def _cb_hp_change(self, event: wx.CommandEvent):
        """
        Callback handling when HP changes.

        :param event: CommandEvent object containing details about the event.
        """
        try:
            v = ui_helpers.get_textbox_int(self.textboxes.hp)
            bloodied_str = self.BLOODIED_STR.format(v // 2)
            self.labels.bloodied.SetLabel(bloodied_str)
        except ValueError:
            pass

    # noinspection PyUnusedLocal
    def _cb_level_change(self, event: wx.CommandEvent):
        """
        Callback handling when the monster's level changes.

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

    def cb_resist_vuln_clicked(self, event: wx.CommandEvent):
        """
        Callback handling when something in the resist/vuln/immune list gets (un)checked.
        Shows/hides that element's extra controls.

        :param event: CommandEvent object containing details about the event.
        """
        control = event.GetEventObject()
        resist_vuln_label = control.GetLabel()
        self._update_resist_vuln_ui(resist_vuln_label)

    def cb_resist_vuln_radio_clicked(self, event: wx.CommandEvent):
        """
        Callback handling when a resistance/vulnerability type is changed.
        Enables/disables its textbox if it has one.

        :param event: CommandEvent object containing details about the event.
        """
        control = event.GetEventObject()
        resist_vuln_label = control.GetName().split(":")[0]
        self.resist_vuln_opts[resist_vuln_label].repaint()

    # noinspection PyUnusedLocal
    def _cb_role_change(self, event: wx.CommandEvent):
        """
        Callback handling when the monster's role changes.

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

    # noinspection PyUnusedLocal
    def _cb_role_mod_change(self, event: wx.CommandEvent):
        """
        Callback handling when the monster's role mod changes.

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

    def _cb_speed_clicked(self, event: wx.CommandEvent):
        """
        Callback handling when a special speed gets (un)checked.
        Shows/hides the speed's labeled textbox.

        :param event: CommandEvent object containing details about the event.
        """
        index = event.GetInt()
        self._update_speed_ui(index)

    # noinspection PyUnusedLocal
    def _cb_stat_change(self, event: wx.Event):
        """
        Callback handling when an editable stat changes.

        :param event: Various kind of event indicating what changed.
        """
        self.recalculate()

    def _cb_swarm_select(self, event: wx.CommandEvent):
        """
        Callback handling when the user (un)checks the Swarm checkbox.

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

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

    def _attribute_changed(self):
        """
        Handles updating any components that depend on the value of an attribute when one changes.
        """
        self.recalculate()
        self.panels.skills.update()

    def _attribute_custom_check(self, custom_checked: bool):
        """
        Adjusts the UI and contents of the attribute text boxes depending on if the user (un)checked
        the option to enter custom attributes.

        :param custom_checked: True if the custom attributes checkbox is checked, False otherwise.
        """
        self.textboxes.str.Enable(custom_checked)
        self.textboxes.con.Enable(custom_checked)
        self.textboxes.dex.Enable(custom_checked)
        self.textboxes.int.Enable(custom_checked)
        self.textboxes.wis.Enable(custom_checked)
        self.textboxes.cha.Enable(custom_checked)
        if not custom_checked:
            # Reset the attributes to default if the user unchecked custom values.
            self.textboxes.str.SetValue(str(stat_stuff.CALC_VALS.str))
            self.textboxes.con.SetValue(str(stat_stuff.CALC_VALS.con))
            self.textboxes.dex.SetValue(str(stat_stuff.CALC_VALS.dex))
            self.textboxes.int.SetValue(str(stat_stuff.CALC_VALS.int))
            self.textboxes.wis.SetValue(str(stat_stuff.CALC_VALS.wis))
            self.textboxes.cha.SetValue(str(stat_stuff.CALC_VALS.cha))

    def _init_custom_check(self, custom_checked: bool):
        """
        Adjusts the UI and contents of the Initiative text boxes depending on if the user (un)checked
        the option to enter a custom Initiative value.

        :param custom_checked: True if the custom Initiative checkbox is checked, False otherwise.
        """
        self.textboxes.init.Enable(custom_checked)
        if not custom_checked:
            # Reset the Initiative value to default if the user unchecked custom values.
            self.textboxes.init.SetValue(str(stat_stuff.CALC_VALS.init))

    def _level_changed(self):
        """
        Handles updating any components that depend on the monster's level for calculations.
        """
        self.recalculate()
        self.panels.skills.update()

    def _organize_traits_actions(self):
        """
        Ensures that the list of actions & traits are displayed traits first (in order), then actions (in order).
        """
        while not self.sizers.ta_list_sizer.IsEmpty():
            self.sizers.ta_list_sizer.Detach(index=0)  # Just pops them out of the sizer w/o destroying them.
        for trait_id in sorted(self.traits.keys()):
            self.sizers.ta_list_sizer.Add(self.traits[trait_id], flag=wx.EXPAND)
        # Add back the dividing line between traits & actions for visual clarity if it is needed.
        self.lines.ta_line.SetSize(wx.Size(width=self.panels.ta_list.GetSize().GetWidth(), height=10))
        if len(self.traits) > 0 and len(self.actions) > 0:
            self.sizers.ta_list_sizer.Add(self.lines.ta_line, flag=wx.TOP | wx.BOTTOM | wx.EXPAND, border=CTRL_BORDER)
            self.lines.ta_line.Show()
            self.sizers.ta_list_sizer.Show(self.lines.ta_line)
        else:
            self.lines.ta_line.Hide()
            self.sizers.ta_list_sizer.Hide(self.lines.ta_line)
        for action_id in sorted(self.actions.keys()):
            self.sizers.ta_list_sizer.Add(self.actions[action_id], flag=wx.EXPAND)
        for trait in self.traits.values():
            trait.update_ui()
        for action in self.actions.values():
            action.update_ui()

    def _role_change(self, new_role: str):
        """
        When the monster role changes, adjusts the attribute role to match and then
        recalculates all default stats.

        :param new_role: String name of the monster's role.
        """
        self.dropdowns.attribute_role.SetSelection(stat_stuff.ROLES.index(new_role))
        self._update_attribute_role(new_role)
        self.recalculate()

    def _role_mod_change(self, new_role_mod: str):
        """
        When the monster's role mod changes, recalculate default stats
        and check if we should show a note for any special monster roles.

        :param new_role_mod: String name of monster's new role.
        """
        self.labels.role_mod_info.SetLabel(ui_helpers.ROLE_MOD_HELP[new_role_mod])
        self.recalculate()

    def _swarm_changed(self, is_swarm: bool):
        """
        When the user (un)checks the Swarm checkbox, adjust properties
        and UI information accordingly.

        :param is_swarm: True if the monster is now a swarm, False if not.
        """
        self.textboxes.swarm_vuln.Enable(is_swarm)
        self.labels.swarm_vuln.Show(is_swarm)
        self.textboxes.swarm_vuln.Show(is_swarm)
        self.labels.swarm_info.Show(is_swarm)
        self.layout()

    def _update_action_ui(self, action_id: int):
        """
        Adjusts the UI for an action panel with the given ID.

        :param action_id: ID of the action panel to update.
        """
        self.actions[action_id].update_ui()

    def _update_attribute_role(self, role_label: str):
        """
        Resets the monster's attributes to level-appropriate defaults based on the selected role.

        :param role_label: The name of the attribute role that was selected.
        """
        level = int(ui_helpers.get_dropdown_str(self.dropdowns.level))
        role_mod = ui_helpers.get_dropdown_str(self.dropdowns.role_mod)
        if not self.checkboxes.custom_attributes.IsChecked():
            self.textboxes.str.SetValue(str(stat_stuff.calc_str(level, role_label, role_mod)))
            self.textboxes.con.SetValue(str(stat_stuff.calc_con(level, role_label, role_mod)))
            self.textboxes.dex.SetValue(str(stat_stuff.calc_dex(level, role_label, role_mod)))
            self.textboxes.int.SetValue(str(stat_stuff.calc_int(level, role_label, role_mod)))
            self.textboxes.wis.SetValue(str(stat_stuff.calc_wis(level, role_label, role_mod)))
            self.textboxes.cha.SetValue(str(stat_stuff.calc_cha(level, role_label, role_mod)))
        self._attribute_changed()

    def _update_resist_vuln_ui(self, rv_label: str):
        """
        Adjusts the UI for a resist/vuln/immunity textbox & mods based on whether it is checked or not.

        :param rv_label: The name of the ResistVulnControls object to examine.
        """
        if self.resist_vuln_opts[rv_label].checkbox.IsChecked():
            self.resist_vuln_opts[rv_label].show()
            self.resist_vuln_opts[rv_label].repaint()
        else:
            self.resist_vuln_opts[rv_label].hide()
            self.resist_vuln_opts[rv_label].repaint()
        self.resize()

    def _update_speed_ui(self, speed_index: int):
        """
        Adjusts the UI for a special speed's textbox & mods based on whether the speed is checked or not.

        :param speed_index: The index into the speeds checklist to look at.
        """
        if self.checklists.speeds.IsChecked(speed_index):
            self.speeds[speed_index].textbox.Enable()
            self.speeds[speed_index].label.Show()
            self.speeds[speed_index].textbox.Show()
            for mod in self.speeds[speed_index].mods:
                mod.Show()
                self.sizers.speed_values_sizer.Show(mod)
            self.sizers.speed_values_sizer.Show(self.speeds[speed_index].label)
            self.sizers.speed_values_sizer.Show(self.speeds[speed_index].textbox)
        else:
            self.speeds[speed_index].textbox.Disable()
            self.speeds[speed_index].label.Hide()
            self.speeds[speed_index].textbox.Hide()
            for mod in self.speeds[speed_index].mods:
                mod.Hide()
                self.sizers.speed_values_sizer.Hide(mod)
            self.sizers.speed_values_sizer.Hide(self.speeds[speed_index].label)
            self.sizers.speed_values_sizer.Hide(self.speeds[speed_index].textbox)
        self.layout()

    def _update_trait_ui(self, trait_id: int):
        """
        Adjusts the UI for a trait panel based on what kind of trait parameters are selected.

        :param trait_id: ID of the trait panel to update.
        """
        self.traits[trait_id].update_ui()

    def _xp_custom_check(self, custom_checked: bool):
        """
        Adjusts the UI and contents of the XP text boxes depending on if the user (un)checked
        the option to enter a custom XP value.

        :param custom_checked: True if the custom XP checkbox is checked, False otherwise.
        """
        self.textboxes.xp.Enable(custom_checked)
        if not custom_checked:
            # Reset the XP value to default if the user unchecked custom values.
            self.textboxes.xp.SetValue(str(stat_stuff.CALC_VALS.xp))

    def add_action(self) -> ActionPanel:
        """
        Adds a new action to the action list.
        """
        new_action = ActionPanel(container=self, parent=self.panels.ta_list)
        self.actions[new_action.action_id] = new_action
        self._organize_traits_actions()
        self.resize()
        return new_action

    def add_swarm_aura(self) -> TraitPanel:
        """
        Adds a template for a swarm aura "attack" to the trait list.
        """
        new_trait = TraitPanel(container=self, parent=self.panels.ta_list)
        new_trait.type_box.SetStringSelection("Aura")
        new_trait.name_text.SetValue("Swarm Attack")
        new_trait.range.SetValue("1")
        new_trait.text_text.SetValue(ui_helpers.SWARM_AURA_TEMPLATE)
        self.traits[new_trait.trait_id] = new_trait
        self._organize_traits_actions()
        self.resize()
        return new_trait

    def add_trait(self) -> TraitPanel:
        """
        Adds a new trait to the trait list.
        """
        new_trait = TraitPanel(container=self, parent=self.panels.ta_list)
        self.traits[new_trait.trait_id] = new_trait
        self._organize_traits_actions()
        self.resize()
        return new_trait

    def delete_action(self, action_id: int):
        """
        Removes an action from the action list.

        :param action_id: The ID of the action to remove.
        """
        self.sizers.ta_list_sizer.Hide(self.actions[action_id])
        self.sizers.ta_list_sizer.Remove(self.actions[action_id].sizer)
        self.actions[action_id].destroy()
        del self.actions[action_id]
        self._organize_traits_actions()
        self.resize()

    def delete_trait(self, trait_id: int):
        """
        Removes a trait from the trait list.

        :param trait_id: The ID of the trait to remove.
        """
        self.sizers.ta_list_sizer.Hide(self.traits[trait_id])
        self.sizers.ta_list_sizer.Remove(self.traits[trait_id].sizer)
        self.traits[trait_id].destroy()
        del self.traits[trait_id]
        self._organize_traits_actions()
        self.resize()

    def destroy(self):
        """
        Destroys this panel's children, then itself.
        """
        self.DestroyChildren()
        self.Destroy()

    def initialize_stats(self):
        """
        Initializes and calculates all basic stats.
        """
        # Set default level, role, & role mod.
        self.dropdowns.level.SetSelection(0)
        self.dropdowns.role.SetSelection(0)
        self.dropdowns.role_mod.SetSelection(0)
        level = int(ui_helpers.get_dropdown_str(self.dropdowns.level))
        role = ui_helpers.get_dropdown_str(self.dropdowns.role)
        role_mod = ui_helpers.get_dropdown_str(self.dropdowns.role_mod)
        self.labels.role_mod_info.SetLabel(ui_helpers.ROLE_MOD_HELP[role_mod])
        # Set default size, origin, & type.
        self.dropdowns.size.SetSelection(2)
        self.dropdowns.origin.SetSelection(4)
        self.dropdowns.type.SetSelection(2)
        self.checkboxes.swarm.SetValue(False)
        self.textboxes.swarm_vuln.SetValue(str(stat_stuff.SWARM_DEFAULT_VULN))
        self._swarm_changed(self.checkboxes.swarm.IsChecked())
        # Set default attributes.
        self.dropdowns.attribute_role.SetSelection(0)
        attribute_role = ui_helpers.get_dropdown_str(self.dropdowns.attribute_role)
        self.checkboxes.custom_attributes.SetValue(False)
        stat_stuff.CALC_VALS.str = stat_stuff.calc_str(level, attribute_role, role_mod)
        stat_stuff.CALC_VALS.con = stat_stuff.calc_con(level, attribute_role, role_mod)
        stat_stuff.CALC_VALS.dex = stat_stuff.calc_dex(level, attribute_role, role_mod)
        stat_stuff.CALC_VALS.int = stat_stuff.calc_int(level, attribute_role, role_mod)
        stat_stuff.CALC_VALS.wis = stat_stuff.calc_wis(level, attribute_role, role_mod)
        stat_stuff.CALC_VALS.cha = stat_stuff.calc_cha(level, attribute_role, role_mod)
        self.textboxes.str.SetValue(str(stat_stuff.CALC_VALS.str))
        self.textboxes.con.SetValue(str(stat_stuff.CALC_VALS.con))
        self.textboxes.dex.SetValue(str(stat_stuff.CALC_VALS.dex))
        self.textboxes.int.SetValue(str(stat_stuff.CALC_VALS.int))
        self.textboxes.wis.SetValue(str(stat_stuff.CALC_VALS.wis))
        self.textboxes.cha.SetValue(str(stat_stuff.CALC_VALS.cha))
        self.textboxes.str.Enable(False)
        self.textboxes.con.Enable(False)
        self.textboxes.dex.Enable(False)
        self.textboxes.int.Enable(False)
        self.textboxes.wis.Enable(False)
        self.textboxes.cha.Enable(False)
        # Set XP based on level & role.
        stat_stuff.CALC_VALS.xp = stat_stuff.calc_xp(level, role_mod)
        self.checkboxes.custom_xp.SetValue(False)
        self.textboxes.xp.SetValue(str(stat_stuff.CALC_VALS.xp))
        # Set HP based on level & role.
        stat_stuff.CALC_VALS.hp = stat_stuff.calc_base_hp(role, level, role_mod)
        self.textboxes.hp.SetValue(str(stat_stuff.CALC_VALS.hp))
        # Set Defenses based on level & role.
        stat_stuff.CALC_VALS.ac = stat_stuff.calc_ac(role, level)
        self.textboxes.ac.SetValue(str(stat_stuff.CALC_VALS.ac))
        stat_stuff.CALC_VALS.fort = stat_stuff.calc_fort(role, level)
        self.textboxes.fort.SetValue(str(stat_stuff.CALC_VALS.fort))
        stat_stuff.CALC_VALS.ref = stat_stuff.calc_ref(role, level)
        self.textboxes.ref.SetValue(str(stat_stuff.CALC_VALS.ref))
        stat_stuff.CALC_VALS.will = stat_stuff.calc_will(role, level)
        self.textboxes.will.SetValue(str(stat_stuff.CALC_VALS.will))
        # Initiative.
        stat_stuff.CALC_VALS.init = stat_stuff.calc_init(level, role, stat_stuff.CALC_VALS.dex)
        self.checkboxes.custom_init.SetValue(False)
        self.textboxes.init.SetValue(str(stat_stuff.CALC_VALS.init))
        # Clear out alignment, languages, and equipment.
        self.dropdowns.alignment.SetValue("")
        self.textboxes.languages.Clear()
        self.textboxes.equipment.Clear()
        # Reset skills.
        self.panels.skills.reset()
        # Reset senses.
        for sense_name, sense in self.senses.items():
            if sense.textbox is not None:
                sense.textbox.SetValue("")
            sense.checkbox.SetValue(False)
            sense.update_ui()
        # Reset Speed to the default 6.
        self.speeds[LAND_SPEED_KEY].textbox.SetValue(str(stat_stuff.DEFAULT_SPEED))
        # Uncheck Phasing.
        self.checkboxes.phasing.SetValue(False)
        # Reset special speeds.
        for i in range(0, self.checklists.speeds.GetCount()):
            for mod in self.speeds[i].mods:
                mod.SetValue(False)
            self.speeds[i].textbox.SetValue("")
            self.checklists.speeds.Check(item=i, check=False)
            self._update_speed_ui(i)
        # Reset resistances/vulnerabilities/immunities.
        for rv in self.resist_vuln_opts.values():
            rv.checkbox.SetValue(False)
            rv.value_text.Clear()
            if rv.has_rv:
                rv.resist_radio.SetValue(True)
            self._update_resist_vuln_ui(rv.checkbox.GetLabel())
        # Remove and reset any traits.
        for trait_id in list(self.traits.keys()):
            self.delete_trait(trait_id)
        # Remove and reset any actions.
        for action_id in list(self.actions.keys()):
            self.delete_action(action_id)

        self.set_label_vals()

    def layout(self):
        """
        Cause all UI elements associated with this panel and its parent frame to redo their layout.
        Useful if UI elements get resized, hidden, or shown.
        """
        if not self.laying_out:
            self.laying_out = True
            self.panels.stats.Layout()
            self.panels.stats.Fit()
            self.panels.senses.Layout()
            self.panels.senses.Fit()
            self.panels.speeds.Layout()
            self.panels.speeds.Fit()
            self.panels.skills.Layout()
            self.panels.skills.Fit()
            self.panels.resist_vuln_imm.Layout()
            self.panels.resist_vuln_imm.Fit()
            self.panels.properties.Layout()
            self.panels.properties.Fit()
            for trait in self.traits.values():
                trait.Layout()
                trait.Fit()
            for action in self.actions.values():
                action.Layout()
                action.Fit()
            self.panels.traits_actions.Layout()
            self.panels.traits_actions.Fit()
            self.Layout()
            self.Fit()
            self.frame.layout()
            self.laying_out = False

    def move_action_down(self, action_id: int):
        """
        Moves the action with the given ID down one slot on the list of actions.

        :param action_id: Integer ID of the action to move.
        """
        max_id = max(self.actions.keys())
        if action_id == max_id:
            return
        # Find the lower neighbor of the swapping action.
        swap_id = action_id + 1
        while swap_id not in self.actions.keys() and swap_id <= max_id:
            swap_id += 1
        if swap_id > max_id:
            return
        # Exchange IDs and positions.
        self.actions[action_id].set_id(swap_id)
        self.actions[swap_id].set_id(action_id)
        self.actions[-1] = self.actions[action_id]
        self.actions[action_id] = self.actions[swap_id]
        self.actions[swap_id] = self.actions[-1]
        del self.actions[-1]
        self._organize_traits_actions()
        self.resize()

    def move_action_up(self, action_id: int):
        """
        Moves the action with the given ID up one slot on the list of actions.

        :param action_id: Integer ID of the action to move.
        """
        min_id = min(self.actions.keys())
        if action_id == min_id:
            return
        # Find the upper neighbor of the swapping action.
        swap_id = action_id - 1
        while swap_id not in self.actions.keys() and swap_id >= min_id:
            swap_id -= 1
        if swap_id < min_id:
            return
        # Exchange IDs and positions.
        self.actions[action_id].set_id(swap_id)
        self.actions[swap_id].set_id(action_id)
        self.actions[-1] = self.actions[action_id]
        self.actions[action_id] = self.actions[swap_id]
        self.actions[swap_id] = self.actions[-1]
        del self.actions[-1]
        self._organize_traits_actions()
        self.resize()

    def move_trait_down(self, trait_id: int):
        """
        Moves the trait with the given ID down one slot on the list of traits.

        :param trait_id: Integer ID of the trait to move.
        """
        max_id = max(self.traits.keys())
        if trait_id == max_id:
            return
        # Find the lower neighbor of the swapping trait.
        swap_id = trait_id + 1
        while swap_id not in self.traits.keys() and swap_id <= max_id:
            swap_id += 1
        if swap_id > max_id:
            return
        # Exchange IDs and positions.
        self.traits[trait_id].set_id(swap_id)
        self.traits[swap_id].set_id(trait_id)
        self.traits[-1] = self.traits[trait_id]
        self.traits[trait_id] = self.traits[swap_id]
        self.traits[swap_id] = self.traits[-1]
        del self.traits[-1]
        self._organize_traits_actions()
        self.resize()

    def move_trait_up(self, trait_id: int):
        """
        Moves the trait with the given ID up one slot on the list of traits.

        :param trait_id: Integer ID of the trait to move.
        """
        min_id = min(self.traits.keys())
        if trait_id == min_id:
            return
        # Find the upper neighbor of the swapping trait.
        swap_id = trait_id - 1
        while swap_id not in self.traits.keys() and swap_id >= min_id:
            swap_id -= 1
        if swap_id < min_id:
            return
        # Exchange IDs and positions.
        self.traits[trait_id].set_id(swap_id)
        self.traits[swap_id].set_id(trait_id)
        self.traits[-1] = self.traits[trait_id]
        self.traits[trait_id] = self.traits[swap_id]
        self.traits[swap_id] = self.traits[-1]
        del self.traits[-1]
        self._organize_traits_actions()
        self.resize()

    def recalculate(self):
        """
        Recalculates all "suggested value" stats based on user-input fields.
        """
        if not self.recalculating:
            self.recalculating = True
            level = int(ui_helpers.get_dropdown_str(self.dropdowns.level))
            role = ui_helpers.get_dropdown_str(self.dropdowns.role)
            role_mod = ui_helpers.get_dropdown_str(self.dropdowns.role_mod)

            stat_stuff.CALC_VALS.xp = stat_stuff.calc_xp(level, role_mod)
            stat_stuff.CALC_VALS.hp = stat_stuff.calc_base_hp(role, level, role_mod)
            stat_stuff.CALC_VALS.ac = stat_stuff.calc_ac(role, level)
            stat_stuff.CALC_VALS.fort = stat_stuff.calc_fort(role, level)
            stat_stuff.CALC_VALS.ref = stat_stuff.calc_ref(role, level)
            stat_stuff.CALC_VALS.will = stat_stuff.calc_will(role, level)
            attribute_role = ui_helpers.get_dropdown_str(self.dropdowns.attribute_role)
            stat_stuff.CALC_VALS.str = stat_stuff.calc_str(level, attribute_role, role_mod)
            stat_stuff.CALC_VALS.con = stat_stuff.calc_con(level, attribute_role, role_mod)
            stat_stuff.CALC_VALS.dex = stat_stuff.calc_dex(level, attribute_role, role_mod)
            stat_stuff.CALC_VALS.int = stat_stuff.calc_int(level, attribute_role, role_mod)
            stat_stuff.CALC_VALS.wis = stat_stuff.calc_wis(level, attribute_role, role_mod)
            stat_stuff.CALC_VALS.cha = stat_stuff.calc_cha(level, attribute_role, role_mod)
            stat_stuff.CALC_VALS.init = stat_stuff.calc_init(level, role, stat_stuff.CALC_VALS.dex)
            self._xp_custom_check(self.checkboxes.custom_xp.IsChecked())
            self._init_custom_check(self.checkboxes.custom_init.IsChecked())
            self._attribute_custom_check(self.checkboxes.custom_attributes.IsChecked())
            self.panels.skills.recalculate()
            self.set_label_vals()

            self.repaint()
            self.recalculating = False

    def repaint(self):
        """
        Sizes and "redraws" the panel.
        Also calls up to the containing frame's repaint() so it can adjust if this panel's size changes
        as a result of the Layout() and Fit() calls.
        """
        if not self.repainting:
            self.repainting = True
            #self.layout()
            self.repainting = False

    def reset(self):
        """
        Resets all controls to default values, clears out any buildable lists or special properties.
        """
        self.last_save_file = ''
        self.textboxes.name.SetValue('')
        self.initialize_stats()

    def resize(self):
        """
        Calls Layout() for everything and then Fit() to ensure everything has the correct UI size.
        """
        if not self.resizing:
            self.resizing = True
            self.layout()
            self.resizing = False

    def save(self) -> int:
        """
        Saves the data of the current monster to a file. Uses the last file saved to,
        or asks the user for a file name if no file has been specified yet.

        :return: 1 if the file saved successfully, 0 if it was canceled, -1 if it failed.
        """
        return self.save_as(self.last_save_file)

    def save_as(self, filename: str = '') -> int:
        """
        Saves the data of the current monster to a file.
        Prompts the user for a file name to save to if filename is blank.

        :return: 1 if the file saved successfully, 0 if it was cancelled, -1 if it failed.
        """
        result = -1
        save_file = filename
        if filename == '':
            default_save_name = self.textboxes.name.GetValue()
            default_path = os.getcwd()
            if len(self.last_save_file) > 0:
                default_save_name = os.path.splitext(os.path.basename(self.last_save_file))[0]
                default_path = os.path.dirname(file_utils.abs_path(self.last_save_file))
            save_file = wx.FileSelector(message='Save Monster to file', default_path=default_path, default_filename=default_save_name, default_extension='.mon', wildcard='Monster Files (*.mon)|*.mon|All Files (*.*)|*.*', flags=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT, parent=self.frame).strip()

        # Cancel save if the user cancels out of save file dialog.
        if len(save_file) == 0:
            return 0

        self.frame.set_status('Saving to file "' + save_file + '"...')
        try:
            if file_utils.save_monster(save_file, self):
                self.frame.set_status('Monster saved to file "' + save_file + '"')
                # Remember the file saved to so no prompt is necessary for future Save actions.
                self.last_save_file = save_file
                result = 1
            else:
                log_error('Error: Could not save monster to file "' + save_file + '".')
                self.frame.set_status('Error: Could not save monster to file "' + save_file + '".')
        except Exception as ex:
            log_error('Error: Failed to save monster: {0}\n{1}', ex, traceback.format_exc())
            self.frame.set_status('Error: Could not save monster: {0}'.format(ex))

        return result

    def set_inputs_to_calc_vals(self):
        """
        Sets all user input fields to the calculated "default" values
        for them based on current monster stats.
        """
        self.textboxes.xp.SetValue(str(stat_stuff.CALC_VALS.xp))
        self.textboxes.hp.SetValue(str(stat_stuff.CALC_VALS.hp))
        self.textboxes.init.SetValue(str(stat_stuff.CALC_VALS.init))
        self.textboxes.ac.SetValue(str(stat_stuff.CALC_VALS.ac))
        self.textboxes.fort.SetValue(str(stat_stuff.CALC_VALS.fort))
        self.textboxes.ref.SetValue(str(stat_stuff.CALC_VALS.ref))
        self.textboxes.will.SetValue(str(stat_stuff.CALC_VALS.will))
        self.textboxes.str.SetValue(str(stat_stuff.CALC_VALS.str))
        self.textboxes.con.SetValue(str(stat_stuff.CALC_VALS.con))
        self.textboxes.dex.SetValue(str(stat_stuff.CALC_VALS.dex))
        self.textboxes.int.SetValue(str(stat_stuff.CALC_VALS.int))
        self.textboxes.wis.SetValue(str(stat_stuff.CALC_VALS.wis))
        self.textboxes.cha.SetValue(str(stat_stuff.CALC_VALS.cha))
        self.panels.skills.reset()

    def set_label_vals(self):
        """
        Sets the "suggested" values and other hints in labels based
        on current calculated values.
        """
        self.labels.xp.SetLabel("XP" + self.DEFAULT_VAL_STR.format(stat_stuff.CALC_VALS.xp))
        self.labels.hp.SetLabel("HP" + self.DEFAULT_VAL_STR.format(stat_stuff.CALC_VALS.hp))
        self.labels.ac.SetLabel("AC" + self.DEFAULT_VAL_STR.format(stat_stuff.CALC_VALS.ac))
        self.labels.fort.SetLabel("Fort" + self.DEFAULT_VAL_STR.format(stat_stuff.CALC_VALS.fort))
        self.labels.ref.SetLabel("Ref" + self.DEFAULT_VAL_STR.format(stat_stuff.CALC_VALS.ref))
        self.labels.will.SetLabel("Will" + self.DEFAULT_VAL_STR.format(stat_stuff.CALC_VALS.will))
        self.labels.str.SetLabel("Str" + self.DEFAULT_VAL_STR.format(stat_stuff.CALC_VALS.str))
        self.labels.con.SetLabel("Con" + self.DEFAULT_VAL_STR.format(stat_stuff.CALC_VALS.con))
        self.labels.dex.SetLabel("Dex" + self.DEFAULT_VAL_STR.format(stat_stuff.CALC_VALS.dex))
        self.labels.int.SetLabel("Int" + self.DEFAULT_VAL_STR.format(stat_stuff.CALC_VALS.int))
        self.labels.wis.SetLabel("Wis" + self.DEFAULT_VAL_STR.format(stat_stuff.CALC_VALS.wis))
        self.labels.cha.SetLabel("Cha" + self.DEFAULT_VAL_STR.format(stat_stuff.CALC_VALS.cha))
        self.labels.init.SetLabel("Init" + self.DEFAULT_VAL_STR.format(stat_stuff.CALC_VALS.init))
        self.panels.skills.set_label_values()

    def update_ui(self):
        """
        Updates all UI fields after a monster gets loaded.
        """
        if not self.updating_ui:
            self.updating_ui = True
            self.recalculate()
            self.labels.role_mod_info.SetLabel(ui_helpers.ROLE_MOD_HELP[ui_helpers.get_dropdown_str(self.dropdowns.role_mod)])
            self._swarm_changed(self.checkboxes.swarm.IsChecked())
            self.panels.skills.update()
            for sense in stat_stuff.SENSES.keys():
                self.senses[sense].update_ui()
            for i in range(0, len(stat_stuff.SPEEDS)):
                self._update_speed_ui(i)
            for rvi in stat_stuff.RESIST_VULN_IMM:
                self._update_resist_vuln_ui(rvi)
            for i in self.actions.keys():
                self._update_action_ui(i)
            for i in self.traits.keys():
                self._update_trait_ui(i)
            self.set_label_vals()
            self.updating_ui = False


# ----------
# Application window.
# ----------


class MonsterBuilderFrame(ui_helpers.BuilderFrame):
    """
    Main window of the application if running as a single monster builder.
    """

    def __init__(self):
        """
        Constructor for the window. Sets up and initializes UI elements.
        """
        super().__init__(parent=None, id=wx.ID_ANY, title="D&D 4th Edition Monster Builder v{0:s}".format(__version__), style=wx.DEFAULT_FRAME_STYLE & ~(wx.RESIZE_BORDER | wx.MAXIMIZE_BOX))

        # Set up the default control sizes & font now that text size & scaling info is available.
        ui_helpers.setup_ui_helper_constants(self.GetCharWidth(), self.GetCharHeight())

        # Used to track a CallLater object to clear the status bar of messages.
        self.status_clear_timer = None

        self.monster_panel = None
        self.menubar = None
        self.menus = None
        self.menuitems = None
        self.sizer = None
        self.status_bar = None

        # Flag to help prevent unnecessary recursive redraws.
        self.laying_out = False

        self._setup_ui()
        self._initialize()

    def _setup_ui(self):
        """
        Sets up and places all UI elements on the window.
        """
        self.monster_panel = MonsterBuilderPanel(frame=self, parent=self)
        self.menubar = wx.MenuBar()
        self.menus = types.SimpleNamespace()
        self.menuitems = types.SimpleNamespace()
        self.sizer = wx.BoxSizer(orient=wx.VERTICAL)

        self.Bind(event=wx.EVT_CLOSE, handler=self._cb_close)

        # Menus.
        # ==========
        # File menu.
        self.menus.file = wx.Menu()
        self.menuitems.new = self.menus.file.Append(wx.ID_NEW)
        self.Bind(event=wx.EVT_MENU, handler=self._cb_new, source=self.menuitems.new)
        self.menuitems.open = self.menus.file.Append(wx.ID_OPEN)
        self.Bind(event=wx.EVT_MENU, handler=self._cb_open, source=self.menuitems.open)
        self.menuitems.save = self.menus.file.Append(wx.ID_SAVE)
        self.Bind(event=wx.EVT_MENU, handler=self._cb_save, source=self.menuitems.save)
        self.menuitems.saveas = self.menus.file.Append(wx.ID_SAVEAS)
        self.Bind(event=wx.EVT_MENU, handler=self._cb_save_as, source=self.menuitems.saveas)
        self.menus.file.AppendSeparator()
        self.menuitems.exportrtf = wx.MenuItem(parentMenu=self.menus.file, id=wx.ID_ANY, text="Export to &RTF\tCtrl+R", helpString="Export current entity to an RTF file")
        self.Bind(event=wx.EVT_MENU, handler=self._cb_export_rtf, source=self.menuitems.exportrtf)
        self.menus.file.Append(self.menuitems.exportrtf)
        self.menus.file.AppendSeparator()
        self.menuitems.exit = self.menus.file.Append(wx.ID_EXIT, 'E&xit', 'Exit application')
        self.Bind(event=wx.EVT_MENU, handler=self._cb_close, source=self.menuitems.exit)
        self.menubar.Append(self.menus.file, '&File')
        self.SetMenuBar(self.menubar)

        # Status bar.
        # ==========
        self.status_bar = self.CreateStatusBar(number=1, style=wx.STB_DEFAULT_STYLE, id=wx.ID_ANY)

        # Final window sizer setup.
        # ==========
        self.sizer.Add(self.monster_panel, proportion=1, flag=wx.ALL | wx.EXPAND, border=0)

        self.SetSizerAndFit(self.sizer)

        self.Bind(event=wx.EVT_SIZE, handler=self._cb_resize)

    def _initialize(self):
        """
        Gives UI elements their initial values.
        """
        self.new()

        self.repaint()

    # Callbacks
    # ---------

    # noinspection PyUnusedLocal
    def _cb_close(self, event: wx.CloseEvent):
        """
        Callback to handle closing the window.

        :param event: CloseEvent object containing details about the event.
        """
        # Make sure the status update timer is cleared before leaving to avoid race condition.
        self.clear_status()
        self.Destroy()

    # noinspection PyUnusedLocal
    def _cb_export_rtf(self, event: wx.CommandEvent):
        """
        Callback handling the user wanting to export the monster to an RTF file.

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

    # noinspection PyUnusedLocal
    def _cb_new(self, event: wx.CommandEvent):
        """
        Callback handling the user wanting to create a new monster.

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

    # noinspection PyUnusedLocal
    def _cb_open(self, event: wx.CommandEvent):
        """
        Callback handling the user wanting to open a monster file to edit.

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

    # noinspection PyUnusedLocal
    def _cb_resize(self, event: wx.SizeEvent):
        """
        Callback to handle resizing the window.

        :param event: SizeEvent object containing details about the event.
        """
        self.resize()

    # noinspection PyUnusedLocal
    def _cb_save(self, event: wx.CommandEvent):
        """
        Callback handling the user wanting to (re-)save the monster to a file.

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

    # noinspection PyUnusedLocal
    def _cb_save_as(self, event: wx.CommandEvent):
        """
        Callback handling the user wanting to save the monster to a file.

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

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

    def clear_status(self):
        """
        Forcibly clears the contents of the status bar and cancels any waiting status clearing timer.
        """
        self.SetStatusText("")
        if self.status_clear_timer is not None and self.status_clear_timer.IsRunning():
            self.status_clear_timer.Stop()
            self.status_clear_timer = None

    def export_to_rtf(self):
        """
        Exports the data for this monster to an RTF file.
        """
        rtf_file = wx.SaveFileSelector('RTF', 'RTF Documents (*.rtf)|*.rtf|All Files (*.*)|*.*', self.monster_panel.textboxes.name.GetValue() + '.rtf', self).strip()

        if len(rtf_file) > 0:
            done = False
            # noinspection PyBroadException
            try:
                if exporters.export_monster_to_rtf(rtf_file, self.monster_panel):
                    self.set_status('Data exported to file "{0}"'.format(rtf_file))
                    done = True
            except Exception as ex:
                log_error('Error: Failed to export to RTF: {0}\n{1}', ex, traceback.format_exc())
                self.set_status('Error: Could not export RTF data: {0}'.format(ex))
                done = True

            if not done:
                log_error('Error: Could not export data to file "{0}"', rtf_file)
                self.set_status('Error: Could not export data to file "{0}"'.format(rtf_file))

    def layout(self):
        """
        Redo the layout of the frame. Useful if UI elements were added/deleted/resized.
        """
        if not self.laying_out:
            self.laying_out = True
            self.Layout()
            self.Fit()
            self.laying_out = False

    def new(self):
        """
        Resets all controls to default values, clears out any buildable lists or special properties.
        """
        self.set_status('Starting new monster...')
        while not self.sizer.IsEmpty():
            self.sizer.Remove(0)
        self.monster_panel.destroy()
        self.monster_panel = None
        self.monster_panel = MonsterBuilderPanel(frame=self, parent=self)
        self.sizer.Add(self.monster_panel, proportion=1, flag=wx.ALL | wx.EXPAND, border=0)

        self.set_status('New monster started, all stats and properties reset.')

    def open(self):
        """
        Opens a saved monster file for editing, replacing all the contents of this frame
        with whatever's in the file.
        """
        if len(self.monster_panel.textboxes.name.GetValue()) > 0:
            default_open_name = self.monster_panel.textboxes.name.GetValue() + '.mon'
        else:
            default_open_name = ''
        default_path = os.getcwd()
        if len(self.monster_panel.last_save_file) > 0:
            default_open_name = os.path.basename(self.monster_panel.last_save_file)
            default_path = os.path.dirname(file_utils.abs_path(self.monster_panel.last_save_file))
        open_file = wx.FileSelector(message='Choose Monster file to open', default_path=default_path, default_filename=default_open_name, default_extension='.mon', wildcard='Monster Files (*.mon)|*.mon|All Files (*.*)|*.*', flags=wx.FD_OPEN | wx.FD_FILE_MUST_EXIST, parent=self).strip()

        # Cancel open if the user cancels out of open file dialog.
        if len(open_file) == 0:
            return

        self.set_status('Opening monster file "' + open_file + '"...')
        self.monster_panel.reset()
        try:
            if file_utils.load_monster(open_file, self.monster_panel):
                self.set_status('Monster opened from file "' + open_file + '"')
            else:
                log_error('Error: Could not open monster from file "' + open_file + '".')
                self.set_status('Error: Could not open monster from file "' + open_file + '".')
        except Exception as ex:
            log_error('Error: Failed to open monster: {0}\n{1}', ex, traceback.format_exc())
            self.set_status('Error: Could not open monster: {0}'.format(ex))

    def repaint(self):
        """
        Sizes and "redraws" the window.
        """
        self.layout()

    def resize(self):
        """
        Handles the window being resized by telling all children to rearrange themselves.

        The monster panel's resize() method automatically calls our repaint(), so we can omit it here.
        """
        if self.monster_panel is not None:
            self.monster_panel.resize()

    def set_status(self, status_text: str, clear_later: bool = True):
        """
        Sets the status bar to the given text string and sets a timer to clear the status bar later.
        :param status_text: The text to show on the status bar.
        :param clear_later: If True (the default) will set a timer to clear the status bar later, otherwise will leave the text there.
        """
        self.SetStatusText(status_text)
        if self.status_clear_timer is not None and self.status_clear_timer.IsRunning():
            self.status_clear_timer.Stop()
            self.status_clear_timer = None
        if clear_later:
            self.status_clear_timer = wx.CallLater(ui_helpers.STATUS_MESSAGE_TIME, self.clear_status)


# noinspection PyUnusedLocal
def main(argv):
    """
    Main function to run the program as a single monster editor.

    :param argv: Command-line args as sys.argv would provide.
    """
    main_app = wx.App()
    main_frame = MonsterBuilderFrame()
    main_frame.Show()
    main_app.MainLoop()


if __name__ == "__main__":
    main(sys.argv)
