###############################################################################
#
# Styles - A class for writing the Excel XLSX Worksheet file.
#
# SPDX-License-Identifier: BSD-2-Clause
# Copyright 2013-2023, 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):
        """
        Constructor.

        """

        super(Styles, self).__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):
        # 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):
        # 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]

    def _get_palette_color(self, color):
        # Convert the RGB color.
        if color[0] == '#':
            color = color[1:]

        return "FF" + color.upper()

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

    def _write_style_sheet(self):
        # 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):
        # 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):
        # 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:
            if num_fmt_id in format_codes:
                format_code = format_codes[num_fmt_id]
            else:
                format_code = 'General'

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

        self._xml_empty_tag('numFmt', attributes)

    def _write_fonts(self):
        # 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):
        # 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 = self._get_palette_color(xf_format.font_color)
            self._write_color('rgb', color)
        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):
        # 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):
        # 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):
        # Write the <vertAlign> font sub-element.
        attributes = [('val', val)]

        self._xml_empty_tag('vertAlign', attributes)

    def _write_color(self, name, value):
        # Write the <color> element.
        attributes = [(name, value)]

        self._xml_empty_tag('color', attributes)

    def _write_fills(self):
        # 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):
        # 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):
        # 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:
            fg_color = self._get_palette_color(fg_color)
            self._xml_empty_tag('fgColor', [('rgb', fg_color)])

        if bg_color:
            bg_color = self._get_palette_color(bg_color)
            self._xml_empty_tag('bgColor', [('rgb', bg_color)])
        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):
        # 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):
        # 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):
        # 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:
            color = self._get_palette_color(color)
            self._xml_empty_tag('color', [('rgb', color)])
        else:
            self._xml_empty_tag('color', [('auto', 1)])

        self._xml_end_tag(border_type)

    def _write_cell_style_xfs(self):
        # 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):
        # 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):
        # 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):
        # Write the <xf> element.
        num_fmt_id = xf_format.num_format_index
        font_id = xf_format.font_index
        fill_id = xf_format.fill_index
        border_id = xf_format.border_index
        xf_id = xf_format.xf_id
        has_align = 0
        has_protect = 0

        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_align = 1

        # 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_protect = 1

        # Write XF with sub-elements if required.
        if has_align or has_protect:
            self._xml_start_tag('xf', attributes)
            if has_align:
                self._xml_empty_tag('alignment', align)
            if has_protect:
                self._xml_empty_tag('protection', protection)
            self._xml_end_tag('xf')
        else:
            self._xml_empty_tag('xf', attributes)

    def _write_cell_styles(self):
        # 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):
        # Write the <cellStyle> element.
        attributes = [
            ('name', name),
            ('xfId', xf_id),
            ('builtinId', builtin_id),
        ]

        self._xml_empty_tag('cellStyle', attributes)

    def _write_dxfs(self):
        # 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 xf_format in self.dxf_formats:
                self._xml_start_tag('dxf')
                if xf_format.has_dxf_font:
                    self._write_font(xf_format, True)

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

                if xf_format.has_dxf_fill:
                    self._write_fill(xf_format, True)
                if xf_format.has_dxf_border:
                    self._write_border(xf_format, True)
                self._xml_end_tag('dxf')

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

    def _write_table_styles(self):
        # 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):
        # 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):
        # 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:
            self._write_color('rgb', color)

        self._xml_end_tag('mruColors')

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

        self._xml_empty_tag('condense', attributes)

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

        self._xml_empty_tag('extend', attributes)
