# Copyright (c) 2010-2024 openpyxl

from copy import copy

from openpyxl.compat import safe_string
from openpyxl.utils import (
    get_column_letter,
    get_column_interval,
    column_index_from_string,
    range_boundaries,
)
from openpyxl.utils.units import DEFAULT_COLUMN_WIDTH
from openpyxl.descriptors import (
    Integer,
    Float,
    Bool,
    Strict,
    String,
    Alias,
)
from openpyxl.descriptors.serialisable import Serialisable
from openpyxl.styles.styleable import StyleableObject
from openpyxl.utils.bound_dictionary import BoundDictionary
from openpyxl.xml.functions import Element


class Dimension(Strict, StyleableObject):
    """Information about the display properties of a row or column."""
    __fields__ = ('hidden',
                 'outlineLevel',
                 'collapsed',)

    index = Integer()
    hidden = Bool()
    outlineLevel = Integer(allow_none=True)
    outline_level = Alias('outlineLevel')
    collapsed = Bool()
    style = Alias('style_id')


    def __init__(self, index, hidden, outlineLevel,
                 collapsed, worksheet, visible=True, style=None):
        super().__init__(sheet=worksheet, style_array=style)
        self.index = index
        self.hidden = hidden
        self.outlineLevel = outlineLevel
        self.collapsed = collapsed


    def __iter__(self):
        for key in self.__fields__:
            value = getattr(self, key, None)
            if value:
                yield key, safe_string(value)


    def __copy__(self):
        cp = self.__new__(self.__class__)
        attrib = self.__dict__
        attrib['worksheet'] = self.parent
        cp.__init__(**attrib)
        cp._style = copy(self._style)
        return cp


    def __repr__(self):
        return f"<{self.__class__.__name__} Instance, Attributes={dict(self)}>"


class RowDimension(Dimension):
    """Information about the display properties of a row."""

    __fields__ = Dimension.__fields__ + ('ht', 'customFormat', 'customHeight', 's',
                                         'thickBot', 'thickTop')
    r = Alias('index')
    s = Alias('style_id')
    ht = Float(allow_none=True)
    height = Alias('ht')
    thickBot = Bool()
    thickTop = Bool()

    def __init__(self,
                 worksheet,
                 index=0,
                 ht=None,
                 customHeight=None, # do not write
                 s=None,
                 customFormat=None, # do not write
                 hidden=False,
                 outlineLevel=0,
                 outline_level=None,
                 collapsed=False,
                 visible=None,
                 height=None,
                 r=None,
                 spans=None,
                 thickBot=None,
                 thickTop=None,
                 **kw
                 ):
        if r is not None:
            index = r
        if height is not None:
            ht = height
        self.ht = ht
        if visible is not None:
            hidden = not visible
        if outline_level is not None:
            outlineLevel = outline_level
        self.thickBot = thickBot
        self.thickTop = thickTop
        super().__init__(index, hidden, outlineLevel,
                                           collapsed, worksheet, style=s)

    @property
    def customFormat(self):
        """Always true if there is a style for the row"""
        return self.has_style

    @property
    def customHeight(self):
        """Always true if there is a height for the row"""
        return self.ht is not None


class ColumnDimension(Dimension):
    """Information about the display properties of a column."""

    width = Float()
    bestFit = Bool()
    auto_size = Alias('bestFit')
    index = String()
    min = Integer(allow_none=True)
    max = Integer(allow_none=True)
    collapsed = Bool()

    __fields__ = Dimension.__fields__ + ('width', 'bestFit', 'customWidth', 'style',
                                         'min', 'max')

    def __init__(self,
                 worksheet,
                 index='A',
                 width=DEFAULT_COLUMN_WIDTH,
                 bestFit=False,
                 hidden=False,
                 outlineLevel=0,
                 outline_level=None,
                 collapsed=False,
                 style=None,
                 min=None,
                 max=None,
                 customWidth=False, # do not write
                 visible=None,
                 auto_size=None,):
        self.width = width
        self.min = min
        self.max = max
        if visible is not None:
            hidden = not visible
        if auto_size is not None:
            bestFit = auto_size
        self.bestFit = bestFit
        if outline_level is not None:
            outlineLevel = outline_level
        self.collapsed = collapsed
        super().__init__(index, hidden, outlineLevel,
                                              collapsed, worksheet, style=style)


    @property
    def customWidth(self):
        """Always true if there is a width for the column"""
        return bool(self.width)


    def reindex(self):
        """
        Set boundaries for column definition
        """
        if not all([self.min, self.max]):
            self.min = self.max = column_index_from_string(self.index)

    @property
    def range(self):
        """Return the range of cells actually covered"""
        return f"{get_column_letter(self.min)}:{get_column_letter(self.max)}"


    def to_tree(self):
        attrs = dict(self)
        if attrs.keys() != {'min', 'max'}:
            return Element("col", **attrs)


class DimensionHolder(BoundDictionary):
    """
    Allow columns to be grouped
    """

    def __init__(self, worksheet, reference="index", default_factory=None):
        self.worksheet = worksheet
        self.max_outline = None
        self.default_factory = default_factory
        super().__init__(reference, default_factory)


    def group(self, start, end=None, outline_level=1, hidden=False):
        """allow grouping a range of consecutive rows or columns together

        :param start: first row or column to be grouped (mandatory)
        :param end: last row or column to be grouped (optional, default to start)
        :param outline_level: outline level
        :param hidden: should the group be hidden on workbook open or not
        """
        if end is None:
            end = start

        if isinstance(self.default_factory(), ColumnDimension):
            new_dim = self[start]
            new_dim.outline_level = outline_level
            new_dim.hidden = hidden
            work_sequence = get_column_interval(start, end)[1:]
            for column_letter in work_sequence:
                if column_letter in self:
                    del self[column_letter]
            new_dim.min, new_dim.max = map(column_index_from_string, (start, end))
        elif isinstance(self.default_factory(), RowDimension):
            for el in range(start, end + 1):
                new_dim = self.worksheet.row_dimensions[el]
                new_dim.outline_level = outline_level
                new_dim.hidden = hidden


    def to_tree(self):

        def sorter(value):
            value.reindex()
            return value.min

        el = Element('cols')
        outlines = set()

        for col in sorted(self.values(), key=sorter):
            obj = col.to_tree()
            if obj is not None:
                outlines.add(col.outlineLevel)
                el.append(obj)

        if outlines:
            self.max_outline = max(outlines)

        if len(el):
            return el # must have at least one child


class SheetFormatProperties(Serialisable):

    tagname = "sheetFormatPr"

    baseColWidth = Integer(allow_none=True)
    defaultColWidth = Float(allow_none=True)
    defaultRowHeight = Float()
    customHeight = Bool(allow_none=True)
    zeroHeight = Bool(allow_none=True)
    thickTop = Bool(allow_none=True)
    thickBottom = Bool(allow_none=True)
    outlineLevelRow = Integer(allow_none=True)
    outlineLevelCol = Integer(allow_none=True)

    def __init__(self,
                 baseColWidth=8, #according to spec
                 defaultColWidth=None,
                 defaultRowHeight=15,
                 customHeight=None,
                 zeroHeight=None,
                 thickTop=None,
                 thickBottom=None,
                 outlineLevelRow=None,
                 outlineLevelCol=None,
                ):
        self.baseColWidth = baseColWidth
        self.defaultColWidth = defaultColWidth
        self.defaultRowHeight = defaultRowHeight
        self.customHeight = customHeight
        self.zeroHeight = zeroHeight
        self.thickTop = thickTop
        self.thickBottom = thickBottom
        self.outlineLevelRow = outlineLevelRow
        self.outlineLevelCol = outlineLevelCol


class SheetDimension(Serialisable):

    tagname = "dimension"

    ref = String()

    def __init__(self,
                 ref=None,
                ):
        self.ref = ref


    @property
    def boundaries(self):
        return range_boundaries(self.ref)