###############################################################################
#
# Shape - A class for to represent Excel XLSX shape objects.
#
# SPDX-License-Identifier: BSD-2-Clause
#
# Copyright (c) 2013-2025, John McNamara, jmcnamara@cpan.org
#
import copy
from warnings import warn

from xlsxwriter.color import Color


class Shape:
    """
    A class for to represent Excel XLSX shape objects.


    """

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

    def __init__(self, shape_type, name: str, options) -> None:
        """
        Constructor.

        """
        super().__init__()
        self.name = name
        self.shape_type = shape_type
        self.connect = 0
        self.drawing = 0
        self.edit_as = ""
        self.id = 0
        self.text = ""
        self.textlink = ""
        self.stencil = 1
        self.element = -1
        self.start = None
        self.start_index = None
        self.end = None
        self.end_index = None
        self.adjustments = []
        self.start_side = ""
        self.end_side = ""
        self.flip_h = 0
        self.flip_v = 0
        self.rotation = 0
        self.text_rotation = 0
        self.textbox = False

        self.align = None
        self.fill = None
        self.font = None
        self.format = None
        self.line = None

        self._set_options(options)

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

    def _set_options(self, options) -> None:
        self.align = self._get_align_properties(options.get("align"))
        self.fill = self._get_fill_properties(options.get("fill"))
        self.font = self._get_font_properties(options.get("font"))
        self.gradient = self._get_gradient_properties(options.get("gradient"))
        self.line = self._get_line_properties(options)

        self.text_rotation = options.get("text_rotation", 0)

        self.textlink = options.get("textlink", "")
        if self.textlink.startswith("="):
            self.textlink = self.textlink.lstrip("=")

        # Gradient fill overrides solid fill.
        if self.gradient:
            self.fill = None

    ###########################################################################
    #
    # Static methods for processing chart/shape style properties.
    #
    ###########################################################################

    @staticmethod
    def _get_line_properties(options: dict) -> dict:
        # Convert user line properties to the structure required internally.
        if not options.get("line") and not options.get("border"):
            return {"defined": False}

        # Copy the user defined properties since they will be modified.
        # Depending on the context, the Excel UI property may be called 'line'
        # or 'border'. Internally they are the same so we handle both.
        if options.get("line"):
            line = copy.deepcopy(options["line"])
        else:
            line = copy.deepcopy(options["border"])

        dash_types = {
            "solid": "solid",
            "round_dot": "sysDot",
            "square_dot": "sysDash",
            "dash": "dash",
            "dash_dot": "dashDot",
            "long_dash": "lgDash",
            "long_dash_dot": "lgDashDot",
            "long_dash_dot_dot": "lgDashDotDot",
            "dot": "dot",
            "system_dash_dot": "sysDashDot",
            "system_dash_dot_dot": "sysDashDotDot",
        }

        # Check the dash type.
        dash_type = line.get("dash_type")

        if dash_type is not None:
            if dash_type in dash_types:
                line["dash_type"] = dash_types[dash_type]
            else:
                warn(f"Unknown dash type '{dash_type}'")
                return {}

        if line.get("color"):
            line["color"] = Color._from_value(line["color"])

        line["defined"] = True

        return line

    @staticmethod
    def _get_fill_properties(fill):
        # Convert user fill properties to the structure required internally.

        if not fill:
            return {"defined": False}

        # Copy the user defined properties since they will be modified.
        fill = copy.deepcopy(fill)

        if fill.get("color"):
            fill["color"] = Color._from_value(fill["color"])

        fill["defined"] = True

        return fill

    @staticmethod
    def _get_pattern_properties(pattern):
        # Convert user defined pattern to the structure required internally.

        if not pattern:
            return {}

        # Copy the user defined properties since they will be modified.
        pattern = copy.deepcopy(pattern)

        if not pattern.get("pattern"):
            warn("Pattern must include 'pattern'")
            return {}

        if not pattern.get("fg_color"):
            warn("Pattern must include 'fg_color'")
            return {}

        types = {
            "percent_5": "pct5",
            "percent_10": "pct10",
            "percent_20": "pct20",
            "percent_25": "pct25",
            "percent_30": "pct30",
            "percent_40": "pct40",
            "percent_50": "pct50",
            "percent_60": "pct60",
            "percent_70": "pct70",
            "percent_75": "pct75",
            "percent_80": "pct80",
            "percent_90": "pct90",
            "light_downward_diagonal": "ltDnDiag",
            "light_upward_diagonal": "ltUpDiag",
            "dark_downward_diagonal": "dkDnDiag",
            "dark_upward_diagonal": "dkUpDiag",
            "wide_downward_diagonal": "wdDnDiag",
            "wide_upward_diagonal": "wdUpDiag",
            "light_vertical": "ltVert",
            "light_horizontal": "ltHorz",
            "narrow_vertical": "narVert",
            "narrow_horizontal": "narHorz",
            "dark_vertical": "dkVert",
            "dark_horizontal": "dkHorz",
            "dashed_downward_diagonal": "dashDnDiag",
            "dashed_upward_diagonal": "dashUpDiag",
            "dashed_horizontal": "dashHorz",
            "dashed_vertical": "dashVert",
            "small_confetti": "smConfetti",
            "large_confetti": "lgConfetti",
            "zigzag": "zigZag",
            "wave": "wave",
            "diagonal_brick": "diagBrick",
            "horizontal_brick": "horzBrick",
            "weave": "weave",
            "plaid": "plaid",
            "divot": "divot",
            "dotted_grid": "dotGrid",
            "dotted_diamond": "dotDmnd",
            "shingle": "shingle",
            "trellis": "trellis",
            "sphere": "sphere",
            "small_grid": "smGrid",
            "large_grid": "lgGrid",
            "small_check": "smCheck",
            "large_check": "lgCheck",
            "outlined_diamond": "openDmnd",
            "solid_diamond": "solidDmnd",
        }

        # Check for valid types.
        if pattern["pattern"] not in types:
            warn(f"unknown pattern type '{pattern['pattern']}'")
            return {}

        pattern["pattern"] = types[pattern["pattern"]]

        if pattern.get("fg_color"):
            pattern["fg_color"] = Color._from_value(pattern["fg_color"])

        if pattern.get("bg_color"):
            pattern["bg_color"] = Color._from_value(pattern["bg_color"])
        else:
            pattern["bg_color"] = Color("#FFFFFF")

        return pattern

    @staticmethod
    def _get_gradient_properties(gradient):
        # pylint: disable=too-many-return-statements
        # Convert user defined gradient to the structure required internally.

        if not gradient:
            return {}

        # Copy the user defined properties since they will be modified.
        gradient = copy.deepcopy(gradient)

        types = {
            "linear": "linear",
            "radial": "circle",
            "rectangular": "rect",
            "path": "shape",
        }

        # Check the colors array exists and is valid.
        if "colors" not in gradient or not isinstance(gradient["colors"], list):
            warn("Gradient must include colors list")
            return {}

        # Check the colors array has the required number of entries.
        if not 2 <= len(gradient["colors"]) <= 10:
            warn("Gradient colors list must at least 2 values and not more than 10")
            return {}

        if "positions" in gradient:
            # Check the positions array has the right number of entries.
            if len(gradient["positions"]) != len(gradient["colors"]):
                warn("Gradient positions not equal to number of colors")
                return {}

            # Check the positions are in the correct range.
            for pos in gradient["positions"]:
                if not 0 <= pos <= 100:
                    warn("Gradient position must be in the range 0 <= position <= 100")
                    return {}
        else:
            # Use the default gradient positions.
            if len(gradient["colors"]) == 2:
                gradient["positions"] = [0, 100]

            elif len(gradient["colors"]) == 3:
                gradient["positions"] = [0, 50, 100]

            elif len(gradient["colors"]) == 4:
                gradient["positions"] = [0, 33, 66, 100]

            else:
                warn("Must specify gradient positions")
                return {}

        angle = gradient.get("angle")
        if angle:
            if not 0 <= angle < 360:
                warn("Gradient angle must be in the range 0 <= angle < 360")
                return {}
        else:
            gradient["angle"] = 90

        # Check for valid types.
        gradient_type = gradient.get("type")

        if gradient_type is not None:
            if gradient_type in types:
                gradient["type"] = types[gradient_type]
            else:
                warn(f"Unknown gradient type '{gradient_type}")
                return {}
        else:
            gradient["type"] = "linear"

        gradient["colors"] = [Color._from_value(color) for color in gradient["colors"]]

        return gradient

    @staticmethod
    def _get_font_properties(options):
        # Convert user defined font values into private dict values.
        if options is None:
            options = {}

        font = {
            "name": options.get("name"),
            "color": options.get("color"),
            "size": options.get("size", 11),
            "bold": options.get("bold"),
            "italic": options.get("italic"),
            "underline": options.get("underline"),
            "pitch_family": options.get("pitch_family"),
            "charset": options.get("charset"),
            "baseline": options.get("baseline", -1),
            "lang": options.get("lang", "en-US"),
        }

        # Convert font size units.
        if font["size"]:
            font["size"] = int(font["size"] * 100)

        if font.get("color"):
            font["color"] = Color._from_value(font["color"])

        return font

    @staticmethod
    def _get_font_style_attributes(font):
        # _get_font_style_attributes.
        attributes = []

        if not font:
            return attributes

        if font.get("size"):
            attributes.append(("sz", font["size"]))

        if font.get("bold") is not None:
            attributes.append(("b", 0 + font["bold"]))

        if font.get("italic") is not None:
            attributes.append(("i", 0 + font["italic"]))

        if font.get("underline") is not None:
            attributes.append(("u", "sng"))

        if font.get("baseline") != -1:
            attributes.append(("baseline", font["baseline"]))

        return attributes

    @staticmethod
    def _get_font_latin_attributes(font):
        # _get_font_latin_attributes.
        attributes = []

        if not font:
            return attributes

        if font.get("name") is not None:
            attributes.append(("typeface", font["name"]))

        if font.get("pitch_family") is not None:
            attributes.append(("pitchFamily", font["pitch_family"]))

        if font.get("charset") is not None:
            attributes.append(("charset", font["charset"]))

        return attributes

    @staticmethod
    def _get_align_properties(align):
        # Convert user defined align to the structure required internally.
        if not align:
            return {"defined": False}

        # Copy the user defined properties since they will be modified.
        align = copy.deepcopy(align)

        if "vertical" in align:
            align_type = align["vertical"]

            align_types = {
                "top": "top",
                "middle": "middle",
                "bottom": "bottom",
            }

            if align_type in align_types:
                align["vertical"] = align_types[align_type]
            else:
                warn(f"Unknown alignment type '{align_type}'")
                return {"defined": False}

        if "horizontal" in align:
            align_type = align["horizontal"]

            align_types = {
                "left": "left",
                "center": "center",
                "right": "right",
            }

            if align_type in align_types:
                align["horizontal"] = align_types[align_type]
            else:
                warn(f"Unknown alignment type '{align_type}'")
                return {"defined": False}

        align["defined"] = True

        return align
