# -*- coding: utf-8 -*-
"""
Functions to export program data to other file formats.
"""
from PyRTF3 import *
import copy
import types

import encounter_builder  # Used to resolve circular reference in type hints.
import file_utils
import stat_stuff
import monster_builder  # Used to resolve circular reference in type hints.
import ui_helpers

# Colors to use when exporting RTF tables.
RTFColours = StandardColours.Copy()
RTFColours.append(Colour('MonsterHeading', 118, 146, 60))
RTFColours.append(Colour('MonsterSubHeading', 194, 214, 155))

# Extra fonts for RTF tables.
RTFFonts = StandardFonts.Copy()
RTFFonts.append(Font('D&D 4e Icons', 'roman', 0, 1, '050c0102010202020202'))
RTFFonts.append(Font('Wingdings', 'roman', 0, 1, '050a0102010706040705'))


def landscape_dimensions(paper: Paper) -> Paper:
    """
    Returns inverted dimensions of a PyRTF Paper, effectively "landscaping" it.
    Still have to set landscape to true in any applicable sections.
    """
    return Paper(name=paper.Name, code=paper.Code, description=paper.Description, width=paper.Height, height=paper.Width)


# noinspection PyPep8Naming
def make_dnd_style_sheet() -> StyleSheet:
    """
    Creates and returns a PyRTF.Elements.StyleSheet object
    suitable for use when exporting entities to RTF files.
    """
    result = StyleSheet(colours=RTFColours, fonts=RTFFonts)

    NormalText = TextStyle(TextPropertySet(result.Fonts.Arial, 16))
    SmallText = TextStyle(TextPropertySet(result.Fonts.Arial, 12))
    CurText = NormalText.Copy()

    ps = ParagraphStyle('Normal Spaced',
                        text_style=NormalText.Copy(),
                        paragraph_props=ParagraphPropertySet(space_before=0, space_after=30))
    result.ParagraphStyles.append(ps)

    ps = ParagraphStyle('Normal',
                        text_style=NormalText.Copy(),
                        paragraph_props=ParagraphPropertySet(space_before=0, space_after=0))
    result.ParagraphStyles.append(ps)

    ps = ParagraphStyle('Small Spaced',
                        text_style=SmallText.Copy(),
                        paragraph_props=ParagraphPropertySet(space_before=0, space_after=30))
    result.ParagraphStyles.append(ps)

    ps = ParagraphStyle('Small',
                        text_style=SmallText.Copy(),
                        paragraph_props=ParagraphPropertySet(space_before=0, space_after=0))
    result.ParagraphStyles.append(ps)

    CurText.TextPropertySet.SetSize(16).SetBold(True)
    ps = ParagraphStyle('BigHeading',
                        text_style=CurText.Copy(),
                        paragraph_props=ParagraphPropertySet(space_before=0, space_after=0))
    result.ParagraphStyles.append(ps)

    CurText.TextPropertySet.SetSize(12).SetBold(True)
    ps = ParagraphStyle('Heading',
                        text_style=CurText.Copy(),
                        paragraph_props=ParagraphPropertySet(space_before=0, space_after=0))
    result.ParagraphStyles.append(ps)

    CurText.TextPropertySet.SetSize(12).SetBold(True)
    ps = ParagraphStyle('SubHeading',
                        text_style=CurText.Copy(),
                        paragraph_props=ParagraphPropertySet(space_before=0, space_after=0))
    result.ParagraphStyles.append(ps)

    CurText.TextPropertySet.SetSize(12).SetBold(False)
    ps = ParagraphStyle('SubSubHeading',
                        text_style=CurText.Copy(),
                        paragraph_props=ParagraphPropertySet(space_before=0, space_after=0))
    result.ParagraphStyles.append(ps)

    # Styles based on the core styles above but appropriate to sit in shaded cells.
    ps = result.ParagraphStyles.Normal.Copy()
    ps.SetName('Monster Normal')
    ps.TextStyle.TextPropertySet.SetColour(result.Colours.Black)
    result.ParagraphStyles.append(ps)

    ps = result.ParagraphStyles.Small.Copy()
    ps.SetName('Monster Small')
    ps.TextStyle.TextPropertySet.SetColour(result.Colours.Black)
    result.ParagraphStyles.append(ps)

    ps = result.ParagraphStyles.Heading.Copy()
    ps.SetName('Monster Heading')
    ps.TextStyle.TextPropertySet.SetColour(result.Colours.Black)
    result.ParagraphStyles.append(ps)

    ps = result.ParagraphStyles.SubHeading.Copy()
    ps.SetName('Monster SubHeading')
    ps.TextStyle.TextPropertySet.SetColour(result.Colours.Black)
    result.ParagraphStyles.append(ps)

    ps = result.ParagraphStyles.SubSubHeading.Copy()
    ps.SetName('Monster Power Title')
    ps.TextStyle.TextPropertySet.SetColour(result.Colours.Black)
    result.ParagraphStyles.append(ps)

    # Add some more in that are based on the normal template but that
    # have some indenting set that makes them suitable for doing numbered
    normal_numbered = result.ParagraphStyles.Normal.Copy()
    normal_numbered.SetName('Normal Numbered')
    normal_numbered.ParagraphPropertySet.SetFirstLineIndent(TabPropertySet.DEFAULT_WIDTH * -1)
    normal_numbered.ParagraphPropertySet.SetLeftIndent(TabPropertySet.DEFAULT_WIDTH)

    result.ParagraphStyles.append(normal_numbered)

    normal_numbered2 = result.ParagraphStyles.Normal.Copy()
    normal_numbered2.SetName('Normal Numbered 2')
    normal_numbered2.ParagraphPropertySet.SetFirstLineIndent(TabPropertySet.DEFAULT_WIDTH * -1)
    normal_numbered2.ParagraphPropertySet.SetLeftIndent(TabPropertySet.DEFAULT_WIDTH * 2)

    result.ParagraphStyles.append(normal_numbered2)

    # LIST STYLES
    for idx, indent in [(1, TabPS.DEFAULT_WIDTH),
                        (2, TabPS.DEFAULT_WIDTH * 2),
                        (3, TabPS.DEFAULT_WIDTH * 3)]:
        indent = TabPropertySet.DEFAULT_WIDTH
        ps = ParagraphStyle('List %s' % idx,
                            text_style=TextStyle(TextPropertySet(result.Fonts.Arial, 8)),
                            paragraph_props=ParagraphPropertySet(space_before=0, space_after=60, first_line_indent=-indent, left_indent=indent))
        result.ParagraphStyles.append(ps)

    # DEBUG STYLE
    CurText.TextPropertySet.SetBold(False)
    CurText.TextPropertySet.SetItalic(False)
    CurText.TextPropertySet.SetUnderline(False)
    CurText.TextPropertySet.SetFont(result.Fonts.CourierNew)
    CurText.TextPropertySet.SetSize(12)
    CurText.TextPropertySet.SetColour(result.Colours.Red)
    ps = ParagraphStyle('DEBUG',
                        text_style=CurText.Copy(),
                        paragraph_props=ParagraphPropertySet(space_before=0, space_after=0),
                        shading_props=ShadingPropertySet(shading=1000, background=result.Colours.Red))
    result.ParagraphStyles.append(ps)

    return result


def make_dnd_shading(style_sheet) -> AttributedList:
    """
    Creates a PyRTF.PropertySets.AttributedList of pre-made ShadingPropertySet objects
    that can be used for shading text and document elements.
    :param style_sheet: The StyleSheet object for the document.
    """
    result = AttributedList(ShadingPropertySet)
    result.append(ShadingPropertySet(shading=0, background=style_sheet.Colours.White, name='NoShading'))
    result.append(ShadingPropertySet(shading=1000, background=style_sheet.Colours.MonsterHeading, name='MonsterHeading'))
    result.append(ShadingPropertySet(shading=1000, background=style_sheet.Colours.MonsterSubHeading, name='MonsterSubHeading'))
    result.append(ShadingPropertySet(shading=1000, background=style_sheet.Colours.Red, name='DEBUG'))

    return result


# Container for pre-defined border edges.
Edges = types.SimpleNamespace()
Edges.thin = BorderPS(width=20, style=BorderPS.SINGLE)
Edges.thick = BorderPS(width=60, style=BorderPS.SINGLE)

# Container for pre-defined cell borders.
ThinFrames = types.SimpleNamespace()
# top left bottom right
ThinFrames.top_left = FramePS(Edges.thin, Edges.thin, None, None)
ThinFrames.top_mid = FramePS(Edges.thin, None, None, None)
ThinFrames.top_right = FramePS(Edges.thin, None, None, Edges.thin)
ThinFrames.mid_left = FramePS(None, Edges.thin, None, None)
ThinFrames.mid_mid = FramePS(None, None, None, None)
ThinFrames.mid_right = FramePS(None, None, None, Edges.thin)
ThinFrames.bot_left = FramePS(None, Edges.thin, Edges.thin, None)
ThinFrames.bot_mid = FramePS(None, None, Edges.thin, None)
ThinFrames.bot_left = FramePS(None, None, Edges.thin, Edges.thin)
ThinFrames.top_full = FramePS(Edges.thin, Edges.thin, None, Edges.thin)
ThinFrames.mid_full = FramePS(None, Edges.thin, None, Edges.thin)
ThinFrames.bot_full = FramePS(None, Edges.thin, Edges.thin, Edges.thin)
ThinFrames.full_left = FramePS(Edges.thin, Edges.thin, Edges.thin, None)
ThinFrames.full_mid = FramePS(Edges.thin, None, Edges.thin, None)
ThinFrames.full_right = FramePS(Edges.thin, None, Edges.thin, Edges.thin)
ThinFrames.all = FramePS(Edges.thin, Edges.thin, Edges.thin, Edges.thin)
ThinFrames.none = FramePS(None, None, None, None)

# Container for pre-defined text symbols.
Symbols = types.SimpleNamespace()
Symbols.A = TEXT("\x52", font=RTFFonts.Wingdings, bold=False, italic=False, underline=False)  # Aura
Symbols.CIR = TEXT("\xb7", font=RTFFonts.Symbol, bold=False, italic=False, underline=False)  # Circle
Symbols.DIA = TEXT("\xa8", font=RTFFonts.Symbol, bold=False, italic=False, underline=False)  # Diamond
Symbols.M = TEXT("m", font=RTFFonts.DD4eIcons, bold=False, italic=False, underline=False)  # Melee
Symbols.MB = TEXT("M", font=RTFFonts.DD4eIcons, bold=False, italic=False, underline=False)  # Melee Basic
Symbols.R = TEXT("r", font=RTFFonts.DD4eIcons, bold=False, italic=False, underline=False)  # Ranged
Symbols.RB = TEXT("R", font=RTFFonts.DD4eIcons, bold=False, italic=False, underline=False)  # Ranged Basic
Symbols.BUR = TEXT("a", font=RTFFonts.DD4eIcons, bold=False, italic=False, underline=False)  # Burst
Symbols.CLBUR = TEXT("A", font=RTFFonts.DD4eIcons, bold=False, italic=False, underline=False)  # Close Burst
Symbols.BLA = TEXT("c", font=RTFFonts.DD4eIcons, bold=False, italic=False, underline=False)  # Blast
Symbols.CLBLA = TEXT("C", font=RTFFonts.DD4eIcons, bold=False, italic=False, underline=False)  # Close Blast
Symbols.ONE = TEXT("1", font=RTFFonts.DD4eIcons, bold=False, italic=False, underline=False)  # d6 1 face
Symbols.TWO = TEXT("2", font=RTFFonts.DD4eIcons, bold=False, italic=False, underline=False)  # d6 2 face
Symbols.THREE = TEXT("3", font=RTFFonts.DD4eIcons, bold=False, italic=False, underline=False)  # d6 3 face
Symbols.FOUR = TEXT("4", font=RTFFonts.DD4eIcons, bold=False, italic=False, underline=False)  # d6 4 face
Symbols.FIVE = TEXT("5", font=RTFFonts.DD4eIcons, bold=False, italic=False, underline=False)  # d6 5 face
Symbols.SIX = TEXT("6", font=RTFFonts.DD4eIcons, bold=False, italic=False, underline=False)  # d6 6 face
# Helps turn numbers into die faces.
Symbols.DICE = [Symbols.ONE, Symbols.TWO, Symbols.THREE, Symbols.FOUR, Symbols.FIVE, Symbols.SIX]


def _process_markup_text(text: str, paragraph: Paragraph=None, style: ParagraphStyle=None) -> Paragraph:
    """
    Takes the raw text from a "markup supported" text box (e.g., an action or trait panel)
    and transform it into an RTF-compatible text block.

    :param text: The raw text to process.
    :param paragraph: If not None, the existing Paragraph to add this text to. If None, will make a new Paragraph.
    :param style: If Paragraph is None, this is the ParagraphStyle to apply to the newly created Paragraph.
    :return: paragraph updated with the processed contents of text, or a new Paragraph of style if paragraph is None.
    """
    para = paragraph
    if para is None:
        if style is not None:
            para = Paragraph(style)
        else:
            raise ValueError("Cannot process panel text without supplying either a Paragraph or ParagraphStyle.")

    # First split the text up into blocks by bold/italic/underline formatting.
    text_blocks = []
    text_block = ""
    bold = False
    italic = False
    underline = False
    for c in text:
        if c == '*':
            if len(text_block) > 0:
                text_blocks.append([[bold, italic, underline, False], text_block])
                text_block = ""
            if bold:
                bold = False
            else:
                bold = True
        elif c == '`':
            if len(text_block) > 0:
                text_blocks.append([[bold, italic, underline, False], text_block])
                text_block = ""
            if italic:
                italic = False
            else:
                italic = True
        elif c == '_':
            if len(text_block) > 0:
                text_blocks.append([[bold, italic, underline, False], text_block])
                text_block = ""
            if underline:
                underline = False
            else:
                underline = True
        else:
            text_block += c
    if len(text_block) > 0:
        text_blocks.append([[bold, italic, underline, False], text_block])

    # Then split any text blocks with newlines in them into separate blocks.
    text_line_blocks = []
    add_line = False
    for text_block_info in text_blocks:
        block_info = copy.deepcopy(text_block_info[0])
        block_text = text_block_info[1]
        text_line_block = ""
        for c in block_text:
            if c == "\n":
                block_info[3] = add_line
                text_line_blocks.append([copy.deepcopy(block_info), text_line_block])
                text_line_block = ""
                add_line = True
            else:
                text_line_block += c
        if len(text_line_block) > 0:
            block_info[3] = add_line
            text_line_blocks.append([copy.deepcopy(block_info), text_line_block])
            add_line = False

    # Lastly, take all those blocks and add them to the Paragraph.
    for text_line_block in text_line_blocks:
        if text_line_block[0][3]:
            para.append(LINE)
        text_line = TEXT(text_line_block[1], bold=text_line_block[0][0], italic=text_line_block[0][1], underline=text_line_block[0][2])
        para.append(text_line)

    return para


def _add_actions_to_table(actions: list, table: Table, style_sheet: StyleSheet, shading: AttributedList):
    """
    Adds a set of actions as rows to a table.

    :param actions: List of ActionPanel objects to add.
    :param table: Table to add the action rows to.
    :param style_sheet: StyleSheet object associated with the document.
    :param shading: AttributedList of ShadingPropertySet objects to help color the table cells.
    """
    for action in actions:
        # Action title.
        action_title = Paragraph(style_sheet.ParagraphStyles.MonsterPowerTitle)
        # Special symbol, if any.
        range_type_lower = ui_helpers.get_dropdown_str(action.range_type_box).lower()
        if range_type_lower == "melee":
            action_title.append(Symbols.M)
        elif range_type_lower == "melee basic":
            action_title.append(Symbols.MB)
        elif range_type_lower == "ranged":
            action_title.append(Symbols.R)
        elif range_type_lower == "ranged basic":
            action_title.append(Symbols.RB)
        elif range_type_lower == "burst":
            action_title.append(Symbols.BUR)
        elif range_type_lower == "blast":
            action_title.append(Symbols.BLA)
        # Actual action name.
        action_title.append(B("{0:s} ".format(action.name_text.GetValue())))
        # Add in parenthetical keywords if that makes sense.
        keywords = action.get_keywords()
        if len(keywords) > 0:
            action_title.append(TEXT("({0:s}) ".format(", ".join([keyword.lower() for keyword in keywords]))))
        # Allowed frequency.
        frequency = ui_helpers.get_dropdown_str(action.frequency_box)
        frequency_text = action.frequency_text.GetValue()
        if frequency != "":
            action_title.append(Symbols.CIR, B(" {0:s}".format(frequency)))
        # Handle recharge actions.
        if frequency.lower() == "recharge":
            # If a recharge action, see if we can use the dice for it.
            recharge_numeric, recharge_condition = stat_stuff.is_recharge_numeric(frequency_text)
            if recharge_numeric:
                # Yes, use the dice.
                action_title.append(TEXT(" "))
                for die in recharge_condition:
                    action_title.append(Symbols.DICE[die - 1])
            else:
                # No, just output the raw frequency text.
                action_title.append(TEXT(" {0:s}".format(frequency_text)))
        elif frequency_text != "":
            # Otherwise, if there's something in the frequency text output that too.
            action_title.append(TEXT(" {0:s}".format(action.frequency_text.GetValue())))
        c1 = Cell(ThinFrames.mid_full, shading.MonsterSubHeading, action_title)
        c1.SetSpan(2)
        table.AddRow(c1)
        # Action text.
        action_txt = Paragraph(style_sheet.ParagraphStyles.MonsterSmall)
        action_txt = _process_markup_text(action.text_text.GetValue(), paragraph=action_txt)
        c1 = Cell(ThinFrames.mid_full, shading.NoShading, action_txt)
        c1.SetSpan(2)
        table.AddRow(c1)


def _export_monster_to_table(document: Document, shading: AttributedList, monster: 'monster_builder.MonsterBuilderPanel') -> Table:
    """
    Creates an RTF Table out of a monster's data, presumably to add into a section of an RTF document.

    :param document: The Document that will hold the Table. Used to extract style sheet info.
    :param shading: An AttributedList of ShadingPropertySets as created by make_dnd_shading().
    :param monster: The MonsterBuilderPanel object whose data should be exported.
    :return: An RTF Table object representing the monster's data, ready to integrate into a Section of a Document.
    """
    ss = document.StyleSheet
    para_right = ParagraphPropertySet(alignment=ParagraphPropertySet.RIGHT)

    table = Table(int(TabPS.DEFAULT_WIDTH * 3.5),
                  int(TabPS.DEFAULT_WIDTH * 2.5),
                  gap_between_cells=0)

    # Name, Level, Type.
    c1 = Cell(ThinFrames.top_left, shading.MonsterHeading, Paragraph(ss.ParagraphStyles.MonsterHeading, monster.textboxes.name.GetValue()))
    lvl_type_str = 'Level {0}'.format(ui_helpers.get_dropdown_str(monster.dropdowns.level))
    role_mod = ui_helpers.get_dropdown_str(monster.dropdowns.role_mod)
    if role_mod != "Normal":
        lvl_type_str += ' ' + ui_helpers.get_dropdown_str(monster.dropdowns.role_mod)
    lvl_type_str += ' ' + ui_helpers.get_dropdown_str(monster.dropdowns.role)
    c2 = Cell(ThinFrames.top_right, shading.MonsterHeading, Paragraph(ss.ParagraphStyles.MonsterHeading, para_right, lvl_type_str))
    table.AddRow(c1, c2)

    # Origin, XP.
    desc_str = ui_helpers.get_dropdown_str(monster.dropdowns.size)
    desc_str += ' ' + ui_helpers.get_dropdown_str(monster.dropdowns.origin)
    desc_str += ' ' + ui_helpers.get_dropdown_str(monster.dropdowns.type)
    c1 = Cell(ThinFrames.mid_left, shading.MonsterHeading, Paragraph(ss.ParagraphStyles.MonsterSubHeading, desc_str))
    xp_str = 'XP ' + monster.textboxes.xp.GetValue()
    c2 = Cell(ThinFrames.mid_right, shading.MonsterHeading, Paragraph(ss.ParagraphStyles.MonsterSubHeading, para_right, xp_str))
    table.AddRow(c1, c2)

    # HP & defenses; Speeds.
    def_txt = Paragraph(ss.ParagraphStyles.MonsterSmall)
    def_txt.append(B('HP: '), TEXT(monster.textboxes.hp.GetValue()))
    # Minions don't have a bloodied value, everyone else does.
    if ui_helpers.get_dropdown_str(monster.dropdowns.role_mod).lower() != "minion":
        def_txt.append(B('; Bloodied: '), TEXT(str(ui_helpers.get_textbox_int(monster.textboxes.hp) // 2)))
    # Minions and Super Minions get an extra text blurb talking about missed attacks.
    if ui_helpers.get_dropdown_str(monster.dropdowns.role_mod).lower().find("minion") >= 0:
        def_txt.append(TEXT('; missed attacks never damage a minion.'))
    def_txt.append(LINE, B('AC: '), TEXT(monster.textboxes.ac.GetValue()))
    def_txt.append(B('; Fort: '), TEXT(monster.textboxes.fort.GetValue()))
    def_txt.append(B('; Ref: '), TEXT(monster.textboxes.ref.GetValue()))
    def_txt.append(B('; Will: '), TEXT(monster.textboxes.will.GetValue()))
    def_txt.append(LINE, B('Speed '), TEXT(monster.speeds[stat_stuff.LAND_SPEED_KEY].textbox.GetValue()))
    for i in range(0, monster.checklists.speeds.GetCount()):
        if monster.checklists.speeds.IsChecked(i):
            def_txt.append(TEXT(", {0:s} {1:s}".format(stat_stuff.SPEEDS[i], monster.speeds[i].textbox.GetValue())))
            has_mods = False
            first_mod = True
            mod_str = " ("
            for mod in monster.speeds[i].mods:
                if mod.IsChecked():
                    mod_str += "{0:s}{1:s}".format('' if first_mod else ', ', mod.GetLabelText())
                    first_mod = False
                    has_mods = True
            mod_str += ")"
            if has_mods:
                def_txt.append(TEXT(mod_str))
    if monster.checkboxes.phasing.IsChecked():
        def_txt.append(TEXT(', Phasing'))
    c1 = Cell(ThinFrames.mid_left, shading.NoShading, def_txt)
    # Initiative & Perception; Special senses.
    init_percep_txt = Paragraph(ss.ParagraphStyles.MonsterSmall, para_right)
    init_percep_txt.append(B('Init '), TEXT(stat_stuff.signed_value(ui_helpers.get_textbox_int(monster.textboxes.init))))
    init_percep_txt.append(LINE, B('Perception '), TEXT(stat_stuff.signed_value(monster.skills["Perception"].get_int_value())))
    init_percep_txt.append(LINE)
    first_special_sense = True
    for sense_name, sense in monster.senses.items():
        if sense.checkbox.IsChecked():
            init_percep_txt.append(TEXT("{0:s}{1:s}".format('' if first_special_sense else ', ', sense_name)))
            if sense.has_value:
                init_percep_txt.append(TEXT(" {0:s}".format(sense.textbox.GetValue())))
            first_special_sense = False
    c2 = Cell(ThinFrames.mid_right, shading.NoShading, init_percep_txt)
    table.AddRow(c1, c2)

    # Immunities, Resistances, Vulnerabilities; Epic/Solo boosts.
    res_vuln_txt = Paragraph(ss.ParagraphStyles.MonsterSmall)
    # Collect resistances, vulnerabilities, and immunities in groups to output together.
    resists = []
    vulns = []
    immunes = []
    for rvi_name, rvi in monster.resist_vuln_opts.items():
        if rvi.checkbox.IsChecked():
            if rvi.get_rvi_type() == "i":
                immunes.append(rvi_name.lower())
            elif rvi.get_rvi_type() == "v":
                vulns.append((rvi_name.lower(), rvi.get_value()))
            else:
                resists.append((rvi_name.lower(), rvi.get_value()))
    # Swarms get a special resistance & vulnerability added to them.
    if monster.checkboxes.swarm.IsChecked():
        resists.append(("damage from melee and ranged attacks", "half"))
        vulns.append(("from area and close attacks", monster.textboxes.swarm_vuln.GetValue().strip()))
    first_rvi_output = True
    if len(immunes) > 0:
        res_vuln_txt.append(B("Immune "), TEXT(", ".join(immunes)))
        first_rvi_output = False
    if len(resists) > 0:
        if not first_rvi_output:
            res_vuln_txt.append(TEXT("; "))
        res_vuln_txt.append(B("Resist "), TEXT(", ".join(["{0:s} {1:s}".format(val, name) for name, val in resists])))
        first_rvi_output = False
    if len(vulns) > 0:
        if not first_rvi_output:
            res_vuln_txt.append(TEXT("; "))
        res_vuln_txt.append(B("Vulnerable "), TEXT(", ".join(["{0:s} {1:s}".format(val, name) for name, val in vulns])))
        first_rvi_output = False
    lower_role_mod = ui_helpers.get_dropdown_str(monster.dropdowns.role_mod).lower()
    # Elite, Solo, and Super Minion monsters get an extra blurb about their stats.
    if lower_role_mod == "elite":
        if not first_rvi_output:
            res_vuln_txt.append(LINE)
        res_vuln_txt.append(B("Saving Throws "), TEXT("+2; "), B("Action Points "), TEXT("1"))
        first_rvi_output = False
    elif lower_role_mod == "solo":
        if not first_rvi_output:
            res_vuln_txt.append(LINE)
        res_vuln_txt.append(B("Saving Throws "), TEXT("+5; "), B("Action Points "), TEXT("2"))
        first_rvi_output = False
    elif lower_role_mod == "super minion":
        if not first_rvi_output:
            res_vuln_txt.append(LINE)
        res_vuln_txt.append(TEXT("Damaging attacks that don't reduce HP to 0 deal it Bloodied damage instead."))
        first_rvi_output = False
    # Only add this row in the monster's table if there's actually content in it.
    if not first_rvi_output:
        c1 = Cell(ThinFrames.mid_full, shading.NoShading, res_vuln_txt)
        c1.SetSpan(2)
        table.AddRow(c1)

    # Traits. Auras first, then Properties.
    c1 = Cell(ThinFrames.mid_full, shading.MonsterHeading, Paragraph(ss.ParagraphStyles.MonsterSubHeading, B("Traits")))
    c1.SetSpan(2)
    auras = []
    properties = []
    for i in sorted(monster.traits.keys()):
        if ui_helpers.get_dropdown_str(monster.traits[i].type_box).lower() == "aura":
            auras.append(monster.traits[i])
        else:
            properties.append(monster.traits[i])
    # Only add the "Traits" section if there's at least one to list.
    if len(auras) > 0 or len(properties) > 0 or monster.checkboxes.swarm.IsChecked():
        table.AddRow(c1)
        for aura in auras:
            # Aura title.
            aura_title = Paragraph(ss.ParagraphStyles.MonsterPowerTitle)
            aura_title.append(Symbols.A, B(" {0:s} ".format(aura.name_text.GetValue())))
            # Add in parenthetical keywords if that makes sense.
            keywords = aura.get_keywords()
            if len(keywords) > 0:
                aura_title.append(TEXT("({0:s}) ".format(", ".join([keyword.lower() for keyword in keywords]))))
            aura_title.append(Symbols.CIR, B(" Aura "), TEXT("{0:s}".format(aura.range.GetValue())))
            c1 = Cell(ThinFrames.mid_full, shading.MonsterSubHeading, aura_title)
            c1.SetSpan(2)
            table.AddRow(c1)
            # Aura text.
            aura_txt = Paragraph(ss.ParagraphStyles.MonsterSmall)
            aura_txt = _process_markup_text(aura.text_text.GetValue(), paragraph=aura_txt)
            c1 = Cell(ThinFrames.mid_full, shading.NoShading, aura_txt)
            c1.SetSpan(2)
            table.AddRow(c1)
        for prop in properties:
            # Property title.
            prop_title = Paragraph(ss.ParagraphStyles.MonsterPowerTitle)
            prop_title.append(B("{0:s}".format(prop.name_text.GetValue())))
            # Add in parenthetical keywords if that makes sense.
            keywords = prop.get_keywords()
            if len(keywords) > 0:
                prop_title.append(TEXT(" ({0:s})".format(", ".join([keyword.lower() for keyword in keywords]))))
            c1 = Cell(ThinFrames.mid_full, shading.MonsterSubHeading, prop_title)
            c1.SetSpan(2)
            table.AddRow(c1)
            # Property text.
            prop_txt = Paragraph(ss.ParagraphStyles.MonsterSmall)
            prop_txt = _process_markup_text(prop.text_text.GetValue(), paragraph=prop_txt)
            c1 = Cell(ThinFrames.mid_full, shading.NoShading, prop_txt)
            c1.SetSpan(2)
            table.AddRow(c1)
        # Swarms get a special property added to them describing how swarms work.
        if monster.checkboxes.swarm.IsChecked():
            # Swarm property title.
            swarm_prop_title = Paragraph(ss.ParagraphStyles.MonsterPowerTitle)
            swarm_prop_title.append(B("Swarm"))
            c1 = Cell(ThinFrames.mid_full, shading.MonsterSubHeading, swarm_prop_title)
            c1.SetSpan(2)
            table.AddRow(c1)
            # Swarm property text.
            swarm_prop_txt = Paragraph(ss.ParagraphStyles.MonsterSmall)
            swarm_prop_txt = _process_markup_text(ui_helpers.SWARM_TRAIT_DESCRIPTION, paragraph=swarm_prop_txt)
            c1 = Cell(ThinFrames.mid_full, shading.NoShading, swarm_prop_txt)
            c1.SetSpan(2)
            table.AddRow(c1)

    # Actions. Collect them by action type, then add a section for each action type present.
    actions = {"standard": [], "move": [], "minor": [], "triggered": [], "free": [], "other": []}
    for i in sorted(monster.actions.keys()):
        actions[ui_helpers.get_dropdown_str(monster.actions[i].type_box).lower()].append(monster.actions[i])
    # Standard Actions.
    c1 = Cell(ThinFrames.mid_full, shading.MonsterHeading, Paragraph(ss.ParagraphStyles.MonsterSubHeading, B("Standard Actions")))
    c1.SetSpan(2)
    if len(actions["standard"]) > 0:
        table.AddRow(c1)
        _add_actions_to_table(actions["standard"], table, ss, shading)

    # Move Actions.
    c1 = Cell(ThinFrames.mid_full, shading.MonsterHeading, Paragraph(ss.ParagraphStyles.MonsterSubHeading, B("Move Actions")))
    c1.SetSpan(2)
    if len(actions["move"]) > 0:
        table.AddRow(c1)
        _add_actions_to_table(actions["move"], table, ss, shading)

    # Minor Actions.
    c1 = Cell(ThinFrames.mid_full, shading.MonsterHeading, Paragraph(ss.ParagraphStyles.MonsterSubHeading, B("Minor Actions")))
    c1.SetSpan(2)
    if len(actions["minor"]) > 0:
        table.AddRow(c1)
        _add_actions_to_table(actions["minor"], table, ss, shading)

    # Triggered Actions.
    c1 = Cell(ThinFrames.mid_full, shading.MonsterHeading, Paragraph(ss.ParagraphStyles.MonsterSubHeading, B("Triggered Actions")))
    c1.SetSpan(2)
    if len(actions["triggered"]) > 0:
        table.AddRow(c1)
        _add_actions_to_table(actions["triggered"], table, ss, shading)

    # Free Actions.
    c1 = Cell(ThinFrames.mid_full, shading.MonsterHeading, Paragraph(ss.ParagraphStyles.MonsterSubHeading, B("Free Actions")))
    c1.SetSpan(2)
    if len(actions["free"]) > 0:
        table.AddRow(c1)
        _add_actions_to_table(actions["free"], table, ss, shading)

    # Other Actions.
    c1 = Cell(ThinFrames.mid_full, shading.MonsterHeading, Paragraph(ss.ParagraphStyles.MonsterSubHeading, B("Other Actions")))
    c1.SetSpan(2)
    if len(actions["other"]) > 0:
        table.AddRow(c1)
        _add_actions_to_table(actions["other"], table, ss, shading)

    # Skills, and core attributes.
    # Only include skills the user wanted listed, and only include a skills section if there's at least one to list.
    incl_skills = []
    for skill in monster.skills.values():
        if skill.included_checkbox.IsChecked():
            incl_skills.append(skill)
    if len(incl_skills) > 0:
        skills_txt = Paragraph(ss.ParagraphStyles.MonsterPowerTitle)
        skills_txt.append(B("Skills "))
        skills_str = ", ".join(["{0:s} {1:s}".format(s.skill_name, stat_stuff.signed_value(ui_helpers.get_textbox_int(s.value_text))) for s in incl_skills])
        skills_txt.append(TEXT(skills_str))
        c1 = Cell(ThinFrames.mid_full, shading.MonsterSubHeading, skills_txt)
        c1.SetSpan(2)
        table.AddRow(c1)
    # Core attributes always appear, arranged in rows str/dex/wis & con/int/cha.
    attr_txt = Paragraph(ss.ParagraphStyles.MonsterPowerTitle)
    level = int(ui_helpers.get_dropdown_str(monster.dropdowns.level))
    attr_val = ui_helpers.get_textbox_attribute(monster.textboxes.str)
    attr_mod = stat_stuff.calc_attribute_mod(attr_val, level)
    attr_txt.append(B("Str "), TEXT("{0:s} ({1:s})".format(str(attr_val), stat_stuff.signed_value(attr_mod))))
    attr_val = ui_helpers.get_textbox_attribute(monster.textboxes.dex)
    attr_mod = stat_stuff.calc_attribute_mod(attr_val, level)
    attr_txt.append(TAB, TAB, B("Dex "), TEXT("{0:s} ({1:s})".format(str(attr_val), stat_stuff.signed_value(attr_mod))))
    attr_val = ui_helpers.get_textbox_attribute(monster.textboxes.wis)
    attr_mod = stat_stuff.calc_attribute_mod(attr_val, level)
    attr_txt.append(TAB, TAB, B("Wis "), TEXT("{0:s} ({1:s})".format(str(attr_val), stat_stuff.signed_value(attr_mod))))
    attr_val = ui_helpers.get_textbox_attribute(monster.textboxes.con)
    attr_mod = stat_stuff.calc_attribute_mod(attr_val, level)
    attr_txt.append(LINE, B("Con "), TEXT("{0:s} ({1:s})".format(str(attr_val), stat_stuff.signed_value(attr_mod))))
    attr_val = ui_helpers.get_textbox_attribute(monster.textboxes.int)
    attr_mod = stat_stuff.calc_attribute_mod(attr_val, level)
    attr_txt.append(TAB, TAB, B("Int "), TEXT("{0:s} ({1:s})".format(str(attr_val), stat_stuff.signed_value(attr_mod))))
    attr_val = ui_helpers.get_textbox_attribute(monster.textboxes.cha)
    attr_mod = stat_stuff.calc_attribute_mod(attr_val, level)
    attr_txt.append(TAB, TAB, B("Cha "), TEXT("{0:s} ({1:s})".format(str(attr_val), stat_stuff.signed_value(attr_mod))))
    # Figure out if this is the last row of the table, adjust framing accordingly.
    frame_style = ThinFrames.mid_full
    if len(monster.dropdowns.alignment.GetValue().strip()) == 0 and len(monster.textboxes.languages.GetValue().strip()) == 0 and len(monster.textboxes.equipment.GetValue().strip()) == 0:
        frame_style = ThinFrames.bot_full
    c1 = Cell(frame_style, shading.MonsterSubHeading, attr_txt)
    c1.SetSpan(2)
    table.AddRow(c1)

    # Alignment, languages, and equipment.
    align_lang_eqiup_txt = Paragraph(ss.ParagraphStyles.MonsterSmall)
    has_alignment = False
    has_lang = False
    has_equip = False
    if len(monster.dropdowns.alignment.GetValue().strip()) > 0:
        has_alignment = True
        align_lang_eqiup_txt.append(B("Alignment "), TEXT(monster.dropdowns.alignment.GetValue().strip()))
    if len(monster.textboxes.languages.GetValue().strip()) > 0:
        has_lang = True
        if has_alignment:
            align_lang_eqiup_txt.append(LINE)
        align_lang_eqiup_txt.append(B("Languages "), TEXT(monster.textboxes.languages.GetValue().strip()))
    if len(monster.textboxes.equipment.GetValue().strip()) > 0:
        has_equip = True
        if has_alignment or has_lang:
            align_lang_eqiup_txt.append(LINE)
        align_lang_eqiup_txt.append(B("Equipment "), TEXT(monster.textboxes.equipment.GetValue().strip()))
    if has_alignment or has_lang or has_equip:
        c1 = Cell(ThinFrames.bot_full, shading.NoShading, align_lang_eqiup_txt)
        c1.SetSpan(2)
        table.AddRow(c1)

    return table


def export_monster_to_rtf(filename: str, monster: 'monster_builder.MonsterBuilderPanel') -> bool:
    """
    Creates an RTF file containing the given monster's data as a table.

    :param filename: Filename (possibly with path) to export to. Will be overwritten if it already exists.
    :param monster: A MonsterBuilderPanel object whose data should be exported.
    :type monster: MonsterBuilderPanel
    :return: True if the file could be opened and exported to, False otherwise.
    """
    doc = Document(style_sheet=make_dnd_style_sheet(), default_language=Languages.EnglishUS, view_kind=ViewKind.Normal)
    ss = doc.StyleSheet
    shading = make_dnd_shading(ss)
    section = Section(paper=landscape_dimensions(StandardPaper.LETTER), margins=MarginsPropertySet(top=720, left=720, bottom=720, right=720), landscape=True)
    doc.Sections.append(section)

    section.append(_export_monster_to_table(doc, shading, monster))

    rtf_renderer = Renderer()

    with open(file_utils.abs_path(filename), 'wb') as outfile:
        rtf_renderer.Write(doc, outfile)

    return True


def export_encounter_to_rtf(filename: str, encounter: 'encounter_builder.EncounterBuilderPanel') -> bool:
    """
    Creates an RTF file containing the given encounter's data.

    Encounter description and stats will appear as opening paragraphs, and monsters will appear as tables.

    :param filename: Filename (possibly with path) to export to. Will be overwritten if it already exists.
    :param encounter: An EncounterBuilderPanel object whose data should be exported.
    :type encounter: EncounterBuilderPanel
    :return: True if the file could be opened and exported to, False otherwise.
    """
    doc = Document(style_sheet=make_dnd_style_sheet(), default_language=Languages.EnglishUS, view_kind=ViewKind.Normal)
    ss = doc.StyleSheet
    shading = make_dnd_shading(ss)
    section = Section(paper=landscape_dimensions(StandardPaper.LETTER), margins=MarginsPropertySet(top=720, left=720, bottom=720, right=720), landscape=True)
    doc.Sections.append(section)

    # Some stats calculations used in the text below.
    num_unique_monsters = len(encounter.monsters.keys())
    num_monsters = 0
    monster_groups = []
    for monster_ctrl in encounter.panels.manage.monster_ctrls.values():
        if monster_ctrl.is_included():
            monster_quantity = monster_ctrl.get_quantity_int()
            monster_xp = ui_helpers.get_textbox_int(encounter.monsters[monster_ctrl.monster_id].panel.textboxes.xp)
            num_monsters += monster_quantity
            # For each monster in the encounter: how many there are, their total XP, and the monster's name.
            monster_groups.append([monster_quantity, monster_quantity * monster_xp, monster_ctrl.monster_name])

    # Encounter name.
    p = Paragraph(ss.ParagraphStyles.BigHeading)
    if len(encounter.textboxes.name.GetValue()) > 0:
        p.append(TEXT('Encounter: {0:s}'.format(encounter.textboxes.name.GetValue())))
    section.append(p)

    # Encounter properties (level, # players, etc.)
    p = Paragraph(ss.ParagraphStyles.Normal)
    num_players = ui_helpers.get_textbox_int(encounter.textboxes.player_count)
    msg = 'Intended for {0:d} {1:s} at Level {2:s}.'.format(num_players, "player" if num_players == 1 else "players", ui_helpers.get_dropdown_str(encounter.dropdowns.level))
    p.append(TEXT(msg))
    p.append(LINE)
    msg = '{0:d} XP (Encounter target: {1:d} XP)'.format(ui_helpers.get_textbox_int(encounter.textboxes.xp_used), ui_helpers.get_textbox_int(encounter.textboxes.xp_budget))
    msg += ', {0:d} {1:s} ({2:d} unique {3:s})'.format(num_monsters, "monster" if num_monsters == 1 else "monsters", num_unique_monsters, "monster" if num_unique_monsters == 1 else "monsters")
    p.append(TEXT(msg))
    p.append(LINE)
    # List of monsters & quantities.
    msg = ''
    for monster_group in monster_groups:
        if len(msg) > 0:
            msg += ', '
        msg += '{0:d}x {1:s} ({2:d} XP)'.format(monster_group[0], monster_group[2], monster_group[1])
    p.append(TEXT(msg))
    section.append(p)

    # Encounter description. Only include if the user wanted to and it's not empty.
    if encounter.checkboxes.description.IsChecked() and len(encounter.textboxes.description.GetValue()) > 0:
        p = _process_markup_text(encounter.textboxes.description.GetValue(), style=ss.ParagraphStyles.Normal)
        section.append(p)

    # Padding between encounter text & monsters.
    section.append(Paragraph(ss.ParagraphStyles.Normal, TEXT('')))

    # Export all monsters to tables first, makes adding them to the document easier.
    monster_tables = []
    for monster in encounter.monsters.values():
        monster_tables.append(_export_monster_to_table(doc, shading, monster.panel))

    # Table of monster tables, three monsters per row.
    table = Table(int(TabPS.DEFAULT_WIDTH * 7),
                  int(TabPS.DEFAULT_WIDTH * 7),
                  int(TabPS.DEFAULT_WIDTH * 7),
                  gap_between_cells=0)

    # Add each monster table into the container table.
    cells = []
    row_index = 0
    for monster_table in monster_tables:
        cells.append(Cell(ThinFrames.all, shading.NoShading, monster_table))
        if len(cells) >= 3:
            table.AddRow(*cells)
            cells.clear()
            row_index += 1
    # Make sure final row gets output even if it's not full.
    if len(cells) > 0:
        spacer = Cell(ThinFrames.all, shading.NoShading, TEXT(''))
        spacer.SetSpan(3 - len(cells))
        cells.append(spacer)
        table.AddRow(*cells)
        cells.clear()
        row_index += 1

    #section.append(table)

    for monster_table in monster_tables:
        section.append(monster_table)
        section.append(Paragraph(ss.ParagraphStyles.Normal, TEXT('')))

    rtf_renderer = Renderer()

    with open(file_utils.abs_path(filename), 'wb') as outfile:
        rtf_renderer.Write(doc, outfile)

    return True
