###############################################################################
#
# Styles - A class for writing the Excel XLSX Worksheet file.
#
# SPDX-License-Identifier: BSD-2-Clause
#
# Copyright (c) 2013-2025, John McNamara, jmcnamara@cpan.org
#

# Package imports.
from . import xmlwriter


class Styles(xmlwriter.XMLwriter):
    """
    A class for writing the Excel XLSX Styles file.


    """

    ###########################################################################
    #
    # Public API.
    #
    ###########################################################################

    def __init__(self) -> None:
        """
        Constructor.

        """

        super().__init__()

        self.xf_formats = []
        self.palette = []
        self.font_count = 0
        self.num_formats = []
        self.border_count = 0
        self.fill_count = 0
        self.custom_colors = []
        self.dxf_formats = []
        self.has_hyperlink = False
        self.hyperlink_font_id = 0
        self.has_comments = False

    ###########################################################################
    #
    # Private API.
    #
    ###########################################################################

    def _assemble_xml_file(self) -> None:
        # Assemble and write the XML file.

        # Write the XML declaration.
        self._xml_declaration()

        # Add the style sheet.
        self._write_style_sheet()

        # Write the number formats.
        self._write_num_fmts()

        # Write the fonts.
        self._write_fonts()

        # Write the fills.
        self._write_fills()

        # Write the borders element.
        self._write_borders()

        # Write the cellStyleXfs element.
        self._write_cell_style_xfs()

        # Write the cellXfs element.
        self._write_cell_xfs()

        # Write the cellStyles element.
        self._write_cell_styles()

        # Write the dxfs element.
        self._write_dxfs()

        # Write the tableStyles element.
        self._write_table_styles()

        # Write the colors element.
        self._write_colors()

        # Close the style sheet tag.
        self._xml_end_tag("styleSheet")

        # Close the file.
        self._xml_close()

    def _set_style_properties(self, properties) -> None:
        # Pass in the Format objects and other properties used in the styles.

        self.xf_formats = properties[0]
        self.palette = properties[1]
        self.font_count = properties[2]
        self.num_formats = properties[3]
        self.border_count = properties[4]
        self.fill_count = properties[5]
        self.custom_colors = properties[6]
        self.dxf_formats = properties[7]
        self.has_comments = properties[8]

    ###########################################################################
    #
    # XML methods.
    #
    ###########################################################################

    def _write_style_sheet(self) -> None:
        # Write the <styleSheet> element.
        xmlns = "http://schemas.openxmlformats.org/spreadsheetml/2006/main"

        attributes = [("xmlns", xmlns)]
        self._xml_start_tag("styleSheet", attributes)

    def _write_num_fmts(self) -> None:
        # Write the <numFmts> element.
        if not self.num_formats:
            return

        attributes = [("count", len(self.num_formats))]
        self._xml_start_tag("numFmts", attributes)

        # Write the numFmts elements.
        for index, num_format in enumerate(self.num_formats, 164):
            self._write_num_fmt(index, num_format)

        self._xml_end_tag("numFmts")

    def _write_num_fmt(self, num_fmt_id, format_code) -> None:
        # Write the <numFmt> element.
        format_codes = {
            0: "General",
            1: "0",
            2: "0.00",
            3: "#,##0",
            4: "#,##0.00",
            5: "($#,##0_);($#,##0)",
            6: "($#,##0_);[Red]($#,##0)",
            7: "($#,##0.00_);($#,##0.00)",
            8: "($#,##0.00_);[Red]($#,##0.00)",
            9: "0%",
            10: "0.00%",
            11: "0.00E+00",
            12: "# ?/?",
            13: "# ??/??",
            14: "m/d/yy",
            15: "d-mmm-yy",
            16: "d-mmm",
            17: "mmm-yy",
            18: "h:mm AM/PM",
            19: "h:mm:ss AM/PM",
            20: "h:mm",
            21: "h:mm:ss",
            22: "m/d/yy h:mm",
            37: "(#,##0_);(#,##0)",
            38: "(#,##0_);[Red](#,##0)",
            39: "(#,##0.00_);(#,##0.00)",
            40: "(#,##0.00_);[Red](#,##0.00)",
            41: '_(* #,##0_);_(* (#,##0);_(* "-"_);_(_)',
            42: '_($* #,##0_);_($* (#,##0);_($* "-"_);_(_)',
            43: '_(* #,##0.00_);_(* (#,##0.00);_(* "-"??_);_(_)',
            44: '_($* #,##0.00_);_($* (#,##0.00);_($* "-"??_);_(_)',
            45: "mm:ss",
            46: "[h]:mm:ss",
            47: "mm:ss.0",
            48: "##0.0E+0",
            49: "@",
        }

        # Set the format code for built-in number formats.
        if num_fmt_id < 164:
            format_code = format_codes.get(num_fmt_id, "General")

        attributes = [
            ("numFmtId", num_fmt_id),
            ("formatCode", format_code),
        ]

        self._xml_empty_tag("numFmt", attributes)

    def _write_fonts(self) -> None:
        # Write the <fonts> element.
        if self.has_comments:
            # Add extra font for comments.
            attributes = [("count", self.font_count + 1)]
        else:
            attributes = [("count", self.font_count)]

        self._xml_start_tag("fonts", attributes)

        # Write the font elements for xf_format objects that have them.
        for xf_format in self.xf_formats:
            if xf_format.has_font:
                self._write_font(xf_format)

        if self.has_comments:
            self._write_comment_font()

        self._xml_end_tag("fonts")

    def _write_font(self, xf_format, is_dxf_format=False) -> None:
        # Write the <font> element.
        self._xml_start_tag("font")

        # The condense and extend elements are mainly used in dxf formats.
        if xf_format.font_condense:
            self._write_condense()

        if xf_format.font_extend:
            self._write_extend()

        if xf_format.bold:
            self._xml_empty_tag("b")

        if xf_format.italic:
            self._xml_empty_tag("i")

        if xf_format.font_strikeout:
            self._xml_empty_tag("strike")

        if xf_format.font_outline:
            self._xml_empty_tag("outline")

        if xf_format.font_shadow:
            self._xml_empty_tag("shadow")

        # Handle the underline variants.
        if xf_format.underline:
            self._write_underline(xf_format.underline)

        if xf_format.font_script == 1:
            self._write_vert_align("superscript")

        if xf_format.font_script == 2:
            self._write_vert_align("subscript")

        if not is_dxf_format:
            self._xml_empty_tag("sz", [("val", xf_format.font_size)])

        if xf_format.theme == -1:
            # Ignore for excel2003_style.
            pass
        elif xf_format.theme:
            self._write_color([("theme", xf_format.theme)])
        elif xf_format.color_indexed:
            self._write_color([("indexed", xf_format.color_indexed)])
        elif xf_format.font_color:
            color = xf_format.font_color
            if not color._is_automatic:
                self._write_color(color._attributes())
        elif not is_dxf_format:
            self._write_color([("theme", 1)])

        if not is_dxf_format:
            self._xml_empty_tag("name", [("val", xf_format.font_name)])

            if xf_format.font_family:
                self._xml_empty_tag("family", [("val", xf_format.font_family)])

            if xf_format.font_charset:
                self._xml_empty_tag("charset", [("val", xf_format.font_charset)])

            if xf_format.font_name == "Calibri" and not xf_format.hyperlink:
                self._xml_empty_tag("scheme", [("val", xf_format.font_scheme)])

            if xf_format.hyperlink:
                self.has_hyperlink = True
                if self.hyperlink_font_id == 0:
                    self.hyperlink_font_id = xf_format.font_index

        self._xml_end_tag("font")

    def _write_comment_font(self) -> None:
        # Write the <font> element for comments.
        self._xml_start_tag("font")

        self._xml_empty_tag("sz", [("val", 8)])
        self._write_color([("indexed", 81)])
        self._xml_empty_tag("name", [("val", "Tahoma")])
        self._xml_empty_tag("family", [("val", 2)])

        self._xml_end_tag("font")

    def _write_underline(self, underline) -> None:
        # Write the underline font element.

        if underline == 2:
            attributes = [("val", "double")]
        elif underline == 33:
            attributes = [("val", "singleAccounting")]
        elif underline == 34:
            attributes = [("val", "doubleAccounting")]
        else:
            # Default to single underline.
            attributes = []

        self._xml_empty_tag("u", attributes)

    def _write_vert_align(self, val) -> None:
        # Write the <vertAlign> font sub-element.
        attributes = [("val", val)]

        self._xml_empty_tag("vertAlign", attributes)

    def _write_color(self, attributes) -> None:
        # Write the <color> element.
        self._xml_empty_tag("color", attributes)

    def _write_fills(self) -> None:
        # Write the <fills> element.
        attributes = [("count", self.fill_count)]

        self._xml_start_tag("fills", attributes)

        # Write the default fill element.
        self._write_default_fill("none")
        self._write_default_fill("gray125")

        # Write the fill elements for xf_format objects that have them.
        for xf_format in self.xf_formats:
            if xf_format.has_fill:
                self._write_fill(xf_format)

        self._xml_end_tag("fills")

    def _write_default_fill(self, pattern_type) -> None:
        # Write the <fill> element for the default fills.
        self._xml_start_tag("fill")
        self._xml_empty_tag("patternFill", [("patternType", pattern_type)])
        self._xml_end_tag("fill")

    def _write_fill(self, xf_format, is_dxf_format=False) -> None:
        # Write the <fill> element.
        pattern = xf_format.pattern
        bg_color = xf_format.bg_color
        fg_color = xf_format.fg_color

        # Colors for dxf formats are handled differently from normal formats
        # since the normal xf_format reverses the meaning of BG and FG for
        # solid fills.
        if is_dxf_format:
            bg_color = xf_format.dxf_bg_color
            fg_color = xf_format.dxf_fg_color

        patterns = (
            "none",
            "solid",
            "mediumGray",
            "darkGray",
            "lightGray",
            "darkHorizontal",
            "darkVertical",
            "darkDown",
            "darkUp",
            "darkGrid",
            "darkTrellis",
            "lightHorizontal",
            "lightVertical",
            "lightDown",
            "lightUp",
            "lightGrid",
            "lightTrellis",
            "gray125",
            "gray0625",
        )

        # Special handling for pattern only case.
        if not fg_color and not bg_color and patterns[pattern]:
            self._write_default_fill(patterns[pattern])
            return

        self._xml_start_tag("fill")

        # The "none" pattern is handled differently for dxf formats.
        if is_dxf_format and pattern <= 1:
            self._xml_start_tag("patternFill")
        else:
            self._xml_start_tag("patternFill", [("patternType", patterns[pattern])])

        if fg_color:
            if not fg_color._is_automatic:
                self._xml_empty_tag("fgColor", fg_color._attributes())

        if bg_color:
            if not bg_color._is_automatic:
                self._xml_empty_tag("bgColor", bg_color._attributes())
        else:
            if not is_dxf_format and pattern <= 1:
                self._xml_empty_tag("bgColor", [("indexed", 64)])

        self._xml_end_tag("patternFill")
        self._xml_end_tag("fill")

    def _write_borders(self) -> None:
        # Write the <borders> element.
        attributes = [("count", self.border_count)]

        self._xml_start_tag("borders", attributes)

        # Write the border elements for xf_format objects that have them.
        for xf_format in self.xf_formats:
            if xf_format.has_border:
                self._write_border(xf_format)

        self._xml_end_tag("borders")

    def _write_border(self, xf_format, is_dxf_format=False) -> None:
        # Write the <border> element.
        attributes = []

        # Diagonal borders add attributes to the <border> element.
        if xf_format.diag_type == 1:
            attributes.append(("diagonalUp", 1))
        elif xf_format.diag_type == 2:
            attributes.append(("diagonalDown", 1))
        elif xf_format.diag_type == 3:
            attributes.append(("diagonalUp", 1))
            attributes.append(("diagonalDown", 1))

        # Ensure that a default diag border is set if the diag type is set.
        if xf_format.diag_type and not xf_format.diag_border:
            xf_format.diag_border = 1

        # Write the start border tag.
        self._xml_start_tag("border", attributes)

        # Write the <border> sub elements.
        self._write_sub_border("left", xf_format.left, xf_format.left_color)

        self._write_sub_border("right", xf_format.right, xf_format.right_color)

        self._write_sub_border("top", xf_format.top, xf_format.top_color)

        self._write_sub_border("bottom", xf_format.bottom, xf_format.bottom_color)

        # Condition DXF formats don't allow diagonal borders.
        if not is_dxf_format:
            self._write_sub_border(
                "diagonal", xf_format.diag_border, xf_format.diag_color
            )

        if is_dxf_format:
            self._write_sub_border("vertical", None, None)
            self._write_sub_border("horizontal", None, None)

        self._xml_end_tag("border")

    def _write_sub_border(self, border_type, style, color) -> None:
        # Write the <border> sub elements such as <right>, <top>, etc.
        attributes = []

        if not style:
            self._xml_empty_tag(border_type)
            return

        border_styles = (
            "none",
            "thin",
            "medium",
            "dashed",
            "dotted",
            "thick",
            "double",
            "hair",
            "mediumDashed",
            "dashDot",
            "mediumDashDot",
            "dashDotDot",
            "mediumDashDotDot",
            "slantDashDot",
        )

        attributes.append(("style", border_styles[style]))

        self._xml_start_tag(border_type, attributes)

        if color and not color._is_automatic:
            self._xml_empty_tag("color", color._attributes())
        else:
            self._xml_empty_tag("color", [("auto", 1)])

        self._xml_end_tag(border_type)

    def _write_cell_style_xfs(self) -> None:
        # Write the <cellStyleXfs> element.
        count = 1

        if self.has_hyperlink:
            count = 2

        attributes = [("count", count)]

        self._xml_start_tag("cellStyleXfs", attributes)
        self._write_style_xf()

        if self.has_hyperlink:
            self._write_style_xf(True, self.hyperlink_font_id)

        self._xml_end_tag("cellStyleXfs")

    def _write_cell_xfs(self) -> None:
        # Write the <cellXfs> element.
        formats = self.xf_formats

        # Workaround for when the last xf_format is used for the comment font
        # and shouldn't be used for cellXfs.
        last_format = formats[-1]
        if last_format.font_only:
            formats.pop()

        attributes = [("count", len(formats))]
        self._xml_start_tag("cellXfs", attributes)

        # Write the xf elements.
        for xf_format in formats:
            self._write_xf(xf_format)

        self._xml_end_tag("cellXfs")

    def _write_style_xf(self, has_hyperlink=False, font_id=0) -> None:
        # Write the style <xf> element.
        num_fmt_id = 0
        fill_id = 0
        border_id = 0

        attributes = [
            ("numFmtId", num_fmt_id),
            ("fontId", font_id),
            ("fillId", fill_id),
            ("borderId", border_id),
        ]

        if has_hyperlink:
            attributes.append(("applyNumberFormat", 0))
            attributes.append(("applyFill", 0))
            attributes.append(("applyBorder", 0))
            attributes.append(("applyAlignment", 0))
            attributes.append(("applyProtection", 0))

            self._xml_start_tag("xf", attributes)
            self._xml_empty_tag("alignment", [("vertical", "top")])
            self._xml_empty_tag("protection", [("locked", 0)])
            self._xml_end_tag("xf")

        else:
            self._xml_empty_tag("xf", attributes)

    def _write_xf(self, xf_format) -> None:
        # Write the <xf> element.
        xf_id = xf_format.xf_id
        font_id = xf_format.font_index
        fill_id = xf_format.fill_index
        border_id = xf_format.border_index
        num_fmt_id = xf_format.num_format_index

        has_checkbox = xf_format.checkbox
        has_alignment = False
        has_protection = False

        attributes = [
            ("numFmtId", num_fmt_id),
            ("fontId", font_id),
            ("fillId", fill_id),
            ("borderId", border_id),
            ("xfId", xf_id),
        ]

        if xf_format.quote_prefix:
            attributes.append(("quotePrefix", 1))

        if xf_format.num_format_index > 0:
            attributes.append(("applyNumberFormat", 1))

        # Add applyFont attribute if XF format uses a font element.
        if xf_format.font_index > 0 and not xf_format.hyperlink:
            attributes.append(("applyFont", 1))

        # Add applyFill attribute if XF format uses a fill element.
        if xf_format.fill_index > 0:
            attributes.append(("applyFill", 1))

        # Add applyBorder attribute if XF format uses a border element.
        if xf_format.border_index > 0:
            attributes.append(("applyBorder", 1))

        # Check if XF format has alignment properties set.
        (apply_align, align) = xf_format._get_align_properties()

        # Check if an alignment sub-element should be written.
        if apply_align and align:
            has_alignment = True

        # We can also have applyAlignment without a sub-element.
        if apply_align or xf_format.hyperlink:
            attributes.append(("applyAlignment", 1))

        # Check for cell protection properties.
        protection = xf_format._get_protection_properties()

        if protection or xf_format.hyperlink:
            attributes.append(("applyProtection", 1))

            if not xf_format.hyperlink:
                has_protection = True

        # Write XF with sub-elements if required.
        if has_alignment or has_protection or has_checkbox:
            self._xml_start_tag("xf", attributes)

            if has_alignment:
                self._xml_empty_tag("alignment", align)

            if has_protection:
                self._xml_empty_tag("protection", protection)

            if has_checkbox:
                self._write_xf_format_extensions()

            self._xml_end_tag("xf")
        else:
            self._xml_empty_tag("xf", attributes)

    def _write_cell_styles(self) -> None:
        # Write the <cellStyles> element.
        count = 1

        if self.has_hyperlink:
            count = 2

        attributes = [("count", count)]

        self._xml_start_tag("cellStyles", attributes)

        if self.has_hyperlink:
            self._write_cell_style("Hyperlink", 1, 8)

        self._write_cell_style()

        self._xml_end_tag("cellStyles")

    def _write_cell_style(self, name="Normal", xf_id=0, builtin_id=0) -> None:
        # Write the <cellStyle> element.
        attributes = [
            ("name", name),
            ("xfId", xf_id),
            ("builtinId", builtin_id),
        ]

        self._xml_empty_tag("cellStyle", attributes)

    def _write_dxfs(self) -> None:
        # Write the <dxfs> element.
        formats = self.dxf_formats
        count = len(formats)

        attributes = [("count", len(formats))]

        if count:
            self._xml_start_tag("dxfs", attributes)

            # Write the font elements for xf_format objects that have them.
            for dxf_format in self.dxf_formats:
                self._xml_start_tag("dxf")
                if dxf_format.has_dxf_font:
                    self._write_font(dxf_format, True)

                if dxf_format.num_format_index:
                    self._write_num_fmt(
                        dxf_format.num_format_index, dxf_format.num_format
                    )

                if dxf_format.has_dxf_fill:
                    self._write_fill(dxf_format, True)

                if dxf_format.has_dxf_border:
                    self._write_border(dxf_format, True)

                if dxf_format.checkbox:
                    self._write_dxf_format_extensions()

                self._xml_end_tag("dxf")

            self._xml_end_tag("dxfs")
        else:
            self._xml_empty_tag("dxfs", attributes)

    def _write_table_styles(self) -> None:
        # Write the <tableStyles> element.
        count = 0
        default_table_style = "TableStyleMedium9"
        default_pivot_style = "PivotStyleLight16"

        attributes = [
            ("count", count),
            ("defaultTableStyle", default_table_style),
            ("defaultPivotStyle", default_pivot_style),
        ]

        self._xml_empty_tag("tableStyles", attributes)

    def _write_colors(self) -> None:
        # Write the <colors> element.
        custom_colors = self.custom_colors

        if not custom_colors:
            return

        self._xml_start_tag("colors")
        self._write_mru_colors(custom_colors)
        self._xml_end_tag("colors")

    def _write_mru_colors(self, custom_colors) -> None:
        # Write the <mruColors> element for the most recently used colors.

        # Write the custom custom_colors in reverse order.
        custom_colors.reverse()

        # Limit the mruColors to the last 10.
        if len(custom_colors) > 10:
            custom_colors = custom_colors[0:10]

        self._xml_start_tag("mruColors")

        # Write the custom custom_colors in reverse order.
        for color in custom_colors:
            # For backwards compatibility convert possible
            self._write_color(color._attributes())

        self._xml_end_tag("mruColors")

    def _write_condense(self) -> None:
        # Write the <condense> element.
        attributes = [("val", 0)]

        self._xml_empty_tag("condense", attributes)

    def _write_extend(self) -> None:
        # Write the <extend> element.
        attributes = [("val", 0)]

        self._xml_empty_tag("extend", attributes)

    def _write_xf_format_extensions(self) -> None:
        # Write the xfComplement <extLst> elements.
        schema = "http://schemas.microsoft.com/office/spreadsheetml"
        attributes = [
            ("uri", "{C7286773-470A-42A8-94C5-96B5CB345126}"),
            (
                "xmlns:xfpb",
                schema + "/2022/featurepropertybag",
            ),
        ]

        self._xml_start_tag("extLst")
        self._xml_start_tag("ext", attributes)

        self._xml_empty_tag("xfpb:xfComplement", [("i", "0")])

        self._xml_end_tag("ext")
        self._xml_end_tag("extLst")

    def _write_dxf_format_extensions(self) -> None:
        # Write the DXFComplement <extLst> elements.
        schema = "http://schemas.microsoft.com/office/spreadsheetml"
        attributes = [
            ("uri", "{0417FA29-78FA-4A13-93AC-8FF0FAFDF519}"),
            (
                "xmlns:xfpb",
                schema + "/2022/featurepropertybag",
            ),
        ]

        self._xml_start_tag("extLst")
        self._xml_start_tag("ext", attributes)

        self._xml_empty_tag("xfpb:DXFComplement", [("i", "0")])

        self._xml_end_tag("ext")
        self._xml_end_tag("extLst")
