# SPDX-License-Identifier: LGPL-2.1-or-later

# ***************************************************************************
# *   Copyright (c) 2009, 2010 Yorik van Havre <yorik@uncreated.net>        *
# *   Copyright (c) 2009, 2010 Ken Cline <cline@frii.com>                   *
# *   Copyright (c) 2020 Eliud Cabrera Castillo <e.cabrera-castillo@tum.de> *
# *   Copyright (c) 2021 FreeCAD Developers                                 *
# *                                                                         *
# *   This file is part of the FreeCAD CAx development system.              *
# *                                                                         *
# *   This program is free software; you can redistribute it and/or modify  *
# *   it under the terms of the GNU Lesser General Public License (LGPL)    *
# *   as published by the Free Software Foundation; either version 2 of     *
# *   the License, or (at your option) any later version.                   *
# *   for detail see the LICENCE text file.                                 *
# *                                                                         *
# *   FreeCAD is distributed in the hope that it will be useful,            *
# *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
# *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
# *   GNU Library General Public License for more details.                  *
# *                                                                         *
# *   You should have received a copy of the GNU Library General Public     *
# *   License along with FreeCAD; if not, write to the Free Software        *
# *   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  *
# *   USA                                                                   *
# *                                                                         *
# ***************************************************************************
"""Provides the viewprovider code for the Label object."""
## @package view_label
# \ingroup draftviewproviders
# \brief Provides the viewprovider code for the Label object.

## \addtogroup draftviewproviders
# @{
import math
import sys
import textwrap
import pivy.coin as coin
from PySide.QtCore import QT_TRANSLATE_NOOP

import FreeCAD as App
from draftutils import gui_utils
from draftutils import params
from draftutils import utils
from draftviewproviders.view_draft_annotation import ViewProviderDraftAnnotation

if App.GuiUp:
    import FreeCADGui as Gui


class ViewProviderLabel(ViewProviderDraftAnnotation):
    """Viewprovider for the Label annotation object."""

    def set_text_properties(self, vobj, properties):
        """Set text properties only if they don't already exist."""
        super().set_text_properties(vobj, properties)

        if "TextAlignment" not in properties:
            _tip = QT_TRANSLATE_NOOP("App::Property", "Vertical alignment")
            vobj.addProperty("App::PropertyEnumeration", "TextAlignment", "Text", _tip, locked=True)
            vobj.TextAlignment = ["Top", "Middle", "Bottom"]
            vobj.TextAlignment = "Bottom"

        if "MaxChars" not in properties:
            _tip = QT_TRANSLATE_NOOP(
                "App::Property", "Maximum number of characters " "on each line of the text box"
            )
            vobj.addProperty("App::PropertyInteger", "MaxChars", "Text", _tip, locked=True)

        if "Justification" not in properties:
            _tip = QT_TRANSLATE_NOOP("App::Property", "Horizontal alignment")
            vobj.addProperty("App::PropertyEnumeration", "Justification", "Text", _tip, locked=True)
            vobj.Justification = ["Left", "Center", "Right"]

        if "LineSpacing" not in properties:
            _tip = QT_TRANSLATE_NOOP("App::Property", "Line spacing (relative to font size)")
            vobj.addProperty("App::PropertyFloat", "LineSpacing", "Text", _tip, locked=True)
            vobj.LineSpacing = params.get_param("LineSpacing")

    def set_graphics_properties(self, vobj, properties):
        """Set graphics properties only if they don't already exist."""
        super().set_graphics_properties(vobj, properties)

        vobj.ArrowTypeStart = params.get_param("dimsymbolstart")

        if "Frame" not in properties:
            _tip = QT_TRANSLATE_NOOP(
                "App::Property", "The type of frame around the text " "of this object"
            )
            vobj.addProperty("App::PropertyEnumeration", "Frame", "Graphics", _tip, locked=True)
            vobj.Frame = ["None", "Rectangle"]

        if "Line" not in properties:
            _tip = QT_TRANSLATE_NOOP("App::Property", "Display a leader line or not")
            vobj.addProperty("App::PropertyBool", "Line", "Graphics", _tip, locked=True)
            vobj.Line = True

    def getIcon(self):
        """Return the path to the icon used by the viewprovider."""
        return ":/icons/Draft_Label.svg"

    def attach(self, vobj):
        """Set up the scene sub-graph of the viewprovider."""
        self.Object = vobj.Object

        # Attributes of the Coin scenegraph
        self.arrow = coin.SoSeparator()
        self.arrowpos = coin.SoTransform()
        self.arrow.addChild(self.arrowpos)

        self.matline = coin.SoMaterial()
        self.drawstyle = coin.SoDrawStyle()
        self.drawstyle.style = coin.SoDrawStyle.LINES

        import PartGui  # Required for "SoBrepEdgeSet" (because a label is not a Part::FeaturePython object).

        self.lcoords = coin.SoCoordinate3()
        self.line = coin.SoType.fromName("SoBrepEdgeSet").createInstance()

        self.mattext = coin.SoMaterial()
        self.textpos = coin.SoTransform()
        self.font = coin.SoFont()
        self.text_wld = coin.SoAsciiText()  # World orientation. Can be oriented in 3D space.
        self.text_scr = coin.SoText2()  # Screen orientation. Always faces the camera.

        self.fcoords = coin.SoCoordinate3()
        self.frame = coin.SoType.fromName("SoBrepEdgeSet").createInstance()
        self.lineswitch = coin.SoSwitch()

        self.startSymbol = gui_utils.dim_symbol()

        textdrawstyle = coin.SoDrawStyle()
        textdrawstyle.style = coin.SoDrawStyle.FILLED

        # The text string needs to be initialized to something,
        # otherwise it crashes
        self.text_wld.string = self.text_scr.string = "Label"
        self.text_wld.justification = coin.SoAsciiText.RIGHT
        self.text_scr.justification = coin.SoText2.RIGHT
        self.font.name = params.get_param("textfont")

        switchnode = coin.SoSeparator()
        switchnode.addChild(self.line)
        self.lineswitch.addChild(switchnode)
        self.lineswitch.whichChild = 0

        self.node_wld_txt = coin.SoGroup()
        self.node_wld_txt.addChild(self.font)
        self.node_wld_txt.addChild(self.text_wld)

        self.node_wld = coin.SoGroup()
        self.node_wld.addChild(self.matline)
        self.node_wld.addChild(self.arrow)
        self.node_wld.addChild(self.drawstyle)
        self.node_wld.addChild(self.lcoords)
        self.node_wld.addChild(self.lineswitch)
        self.node_wld.addChild(self.mattext)
        self.node_wld.addChild(textdrawstyle)
        self.node_wld.addChild(self.textpos)
        self.node_wld.addChild(self.node_wld_txt)
        self.node_wld.addChild(self.matline)
        self.node_wld.addChild(self.drawstyle)
        self.node_wld.addChild(self.fcoords)
        self.node_wld.addChild(self.frame)

        self.node_scr_txt = coin.SoGroup()
        self.node_scr_txt.addChild(self.font)
        self.node_scr_txt.addChild(self.text_scr)

        self.node_scr = coin.SoGroup()
        self.node_scr.addChild(self.matline)
        self.node_scr.addChild(self.arrow)
        self.node_scr.addChild(self.drawstyle)
        self.node_scr.addChild(self.lcoords)
        self.node_scr.addChild(self.lineswitch)
        self.node_scr.addChild(self.mattext)
        self.node_scr.addChild(textdrawstyle)
        self.node_scr.addChild(self.textpos)
        self.node_scr.addChild(self.node_scr_txt)
        self.node_scr.addChild(self.matline)
        self.node_scr.addChild(self.drawstyle)
        self.node_scr.addChild(self.fcoords)
        self.node_scr.addChild(self.frame)

        vobj.addDisplayMode(self.node_wld, "World")
        vobj.addDisplayMode(self.node_scr, "Screen")
        self.onChanged(vobj, "LineColor")
        self.onChanged(vobj, "TextColor")
        self.onChanged(vobj, "LineWidth")
        self.onChanged(vobj, "ArrowSizeStart")
        self.onChanged(vobj, "Line")

    def update_text(self, obj, vobj):
        """Update the text string in the scene, wrapping it if needed."""
        self.text_wld.string.setValue("")
        self.text_scr.string.setValue("")
        _list = [l for l in obj.Text if l] if obj.Text else []

        if _list and hasattr(vobj, "MaxChars") and vobj.MaxChars > 0:
            new_list = []
            for line in _list:
                new_list.extend(textwrap.wrap(line, vobj.MaxChars))
            _list = new_list

        self.text_wld.string.setValues(_list)
        self.text_scr.string.setValues(_list)

    def updateData(self, obj, prop):
        """Execute when a property from the Proxy class is changed."""
        vobj = obj.ViewObject
        if prop == "Points":
            n_points = len(obj.Points)
            if n_points >= 2:
                self.line.coordIndex.deleteValues(0)
                self.lcoords.point.setValues(obj.Points)
                self.line.coordIndex.setValues(0, n_points, range(n_points))
                self.onChanged(obj.ViewObject, "ArrowTypeStart")

            # When initially called (on doc open) properties of vobj are not yet available.
            # This function is however triggered again from update_label.
            if (
                getattr(vobj, "Justification", "") == "Center"
                and obj.StraightDistance != 0.0
                and obj.StraightDirection != "Custom"
            ):
                if obj.StraightDirection != "Vertical":
                    obj.StraightDirection = "Vertical"
                if not hasattr(vobj, "TextAlignment"):
                    pass
                elif obj.StraightDistance > 0.0 and vobj.TextAlignment != "Top":
                    vobj.TextAlignment = "Top"
                elif obj.StraightDistance < 0.0 and vobj.TextAlignment != "Bottom":
                    vobj.TextAlignment = "Bottom"
            elif obj.StraightDistance > 0.0 and obj.StraightDirection == "Horizontal":
                if vobj.Justification == "Left":
                    vobj.Justification = "Right"
            elif obj.StraightDistance < 0.0 and obj.StraightDirection == "Horizontal":
                if vobj.Justification == "Right":
                    vobj.Justification = "Left"

            self.onChanged(vobj, "DisplayMode")  # trigger update_label and update_frame.

        elif prop == "Text" and obj.Text:
            self.update_text(obj, vobj)
            self.onChanged(vobj, "DisplayMode")  # idem

    def onChanged(self, vobj, prop):
        """Execute when a view property is changed."""
        super().onChanged(vobj, prop)

        obj = vobj.Object
        properties = vobj.PropertiesList

        can_update_label = (
            "DisplayMode" in properties
            and "FontName" in properties
            and "FontSize" in properties
            and "Justification" in properties
            and "LineSpacing" in properties
            and "ScaleMultiplier" in properties
            and "TextAlignment" in properties
        )  # Top, Middle or Bottom.
        can_update_frame = can_update_label and "Frame" in properties

        if prop in [
            "DisplayMode",
            "FontName",
            "FontSize",
            "Justification",
            "LineSpacing",
            "TextAlignment",
            "Frame",
        ]:
            if can_update_label:
                self.update_label(obj, vobj)
            if can_update_frame:
                self.update_frame(obj, vobj)

        elif prop == "MaxChars" and "MaxChars" in properties:
            self.update_text(obj, vobj)
            if can_update_label:
                self.update_label(obj, vobj)
            if can_update_frame:
                self.update_frame(obj, vobj)

        elif prop == "ScaleMultiplier" and "ScaleMultiplier" in properties:
            if "ArrowSizeStart" in properties:
                s = vobj.ArrowSizeStart.Value * vobj.ScaleMultiplier
                if s:
                    self.arrowpos.scaleFactor.setValue((s, s, s))
            if can_update_label:
                self.update_label(obj, vobj)
            if can_update_frame:
                self.update_frame(obj, vobj)

        elif (
            prop == "ArrowSizeStart"
            and "ArrowSizeStart" in properties
            and "ScaleMultiplier" in properties
        ):
            s = vobj.ArrowSizeStart.Value * vobj.ScaleMultiplier
            if s:
                self.arrowpos.scaleFactor.setValue((s, s, s))

        elif prop == "ArrowTypeStart" and "ArrowTypeStart" in properties:
            if len(obj.Points) > 1:
                self.update_arrow(obj, vobj)

        elif prop == "Line" and "Line" in properties:
            if vobj.Line:
                self.lineswitch.whichChild = 0
            else:
                self.lineswitch.whichChild = -1

        elif prop == "LineWidth" and "LineWidth" in properties:
            self.drawstyle.lineWidth = vobj.LineWidth

        elif prop == "LineColor" and "LineColor" in properties:
            col = vobj.LineColor
            self.matline.diffuseColor.setValue([col[0], col[1], col[2]])

        elif prop == "TextColor" and "TextColor" in properties:
            col = vobj.TextColor
            self.mattext.diffuseColor.setValue([col[0], col[1], col[2]])

    def get_text_size(self, vobj):
        """Return the bounding box of the text element."""
        if vobj.DisplayMode == "World":
            node = self.node_wld_txt
        else:
            node = self.node_scr_txt

        region = coin.SbViewportRegion()
        action = coin.SoGetBoundingBoxAction(region)
        node.getBoundingBox(action)

        return action.getBoundingBox().getSize().getValue()

    def update_label(self, obj, vobj):
        """Update the label including text size and multiplier."""
        size = vobj.FontSize.Value * vobj.ScaleMultiplier
        line_spacing = max(1, vobj.LineSpacing)
        self.font.size = size
        self.text_wld.spacing = line_spacing
        self.text_scr.spacing = line_spacing
        self.font.name = vobj.FontName.encode("utf8")

        if vobj.Justification == "Left":
            self.text_wld.justification = coin.SoAsciiText.LEFT
            self.text_scr.justification = coin.SoText2.LEFT
            if obj.StraightDistance > 0.0 and obj.StraightDirection == "Horizontal":
                obj.StraightDistance = -obj.StraightDistance
                obj.recompute()
                obj.purgeTouched()
        elif vobj.Justification == "Right":
            self.text_wld.justification = coin.SoAsciiText.RIGHT
            self.text_scr.justification = coin.SoText2.RIGHT
            if obj.StraightDistance < 0.0 and obj.StraightDirection == "Horizontal":
                obj.StraightDistance = -obj.StraightDistance
                obj.recompute()
                obj.purgeTouched()
        else:
            self.text_wld.justification = coin.SoAsciiText.CENTER
            self.text_scr.justification = coin.SoText2.CENTER
            if obj.StraightDistance != 0.0 and obj.StraightDirection != "Custom":
                if obj.StraightDirection != "Vertical":
                    obj.StraightDirection = "Vertical"
                if (obj.StraightDistance < 0.0 and vobj.TextAlignment == "Top") or (
                    obj.StraightDistance > 0.0 and vobj.TextAlignment == "Bottom"
                ):
                    obj.StraightDistance = -obj.StraightDistance
                obj.recompute()
                obj.purgeTouched()

        if vobj.DisplayMode == "Screen":
            self.textpos.translation.setValue(obj.Placement.Base)
            return

        line_height = size * line_spacing
        if vobj.Frame == "None" and vobj.Justification != "Center":
            margin = line_height * 0.1
            first_line_height = size
            # We need to calculate total_height without using get_text_size:
            # If StraightDirection = "Horizontal" and TextAlignment = "Bottom"
            # we want the horizontal line segment to be aligned with the
            # baseline of the bottom text even if there are descenders.
            total_height = first_line_height + (line_height * (len(obj.Text) - 1))
        else:
            margin = line_height * 0.25
            first_line_height = size + margin
            box = self.get_text_size(vobj)
            total_height = box[1] + (2 * margin)

        # Space between endpoint of line and text:
        if vobj.Justification == "Left":
            v = App.Vector(margin, 0, 0)
        elif vobj.Justification == "Right":
            v = App.Vector(-margin, 0, 0)
        else:
            v = App.Vector(0, 0, 0)

        if vobj.TextAlignment == "Top":
            v = v + App.Vector(0, -first_line_height, 0)
        elif vobj.TextAlignment == "Middle":
            v = v + App.Vector(0, -first_line_height + (total_height / 2), 0)
        elif vobj.TextAlignment == "Bottom":
            v = v + App.Vector(0, -first_line_height + total_height, 0)

        v = obj.Placement.Rotation.multVec(v)
        pos = v + obj.Placement.Base
        self.textpos.translation.setValue(pos)
        self.textpos.rotation.setValue(obj.Placement.Rotation.Q)

    def update_arrow(self, obj, vobj):
        """Update the arrow tip of the line."""
        if hasattr(self, "startSymbol"):
            if self.arrow.findChild(self.startSymbol) != -1:
                self.arrow.removeChild(self.startSymbol)

        startS = utils.ARROW_TYPES.index(vobj.ArrowTypeStart)
        self.startSymbol = gui_utils.dim_symbol(startS)
        self.arrow.addChild(self.startSymbol)

        prec = 10 ** (-utils.precision())
        x_axis = App.Vector(1, 0, 0)
        target_dir = None
        # search in Points to get first point != to TargetPoint and use it
        # to get the target line direction
        for pnt in obj.Points[-2::-1]:
            if not pnt.isEqual(obj.Points[-1], prec):
                target_dir = pnt.sub(obj.Points[-1])
                break
        if target_dir is None:
            target_dir = x_axis
        target_dir_xy = obj.Placement.Rotation.inverted() * target_dir
        angle = target_dir_xy.getAngle(x_axis) * App.Units.Radian
        axis = x_axis.cross(target_dir_xy)
        rot = App.Rotation(axis, angle)

        self.arrowpos.rotation.setValue((obj.Placement.Rotation * rot).Q)
        self.arrowpos.translation.setValue(obj.Points[-1])

    def update_frame(self, obj, vobj):
        """Update the frame around the text."""
        self.frame.coordIndex.deleteValues(0)

        if vobj.Frame == "None":
            return
        if vobj.DisplayMode == "Screen":
            return

        size = vobj.FontSize.Value * vobj.ScaleMultiplier
        self.font.size = size

        line_height = size * max(1, vobj.LineSpacing)
        margin = line_height * 0.25
        first_line_height = size + margin
        box = self.get_text_size(vobj)
        total_width = box[0] + (2 * margin)
        total_height = box[1] + (2 * margin)

        # Space between frame and text:
        if vobj.Justification == "Left":
            first_frame_point = App.Vector(-margin, first_line_height, 0)
        elif vobj.Justification == "Right":
            first_frame_point = App.Vector(margin - total_width, first_line_height, 0)
        else:
            first_frame_point = App.Vector(-total_width / 2, first_line_height, 0)

        # Shape of the rectangle
        # (p5)p1 --------- p2
        #     |            |
        #     b            |
        #     |            |
        #     p4 --------- p3
        #
        pts = []
        pts.append(first_frame_point)
        pts.append(first_frame_point + App.Vector(total_width, 0, 0))
        pts.append(pts[-1] + App.Vector(0, -total_height, 0))
        pts.append(pts[-1] + App.Vector(-total_width, 0, 0))
        pts.append(first_frame_point)

        self.fcoords.point.setValues(pts)
        self.frame.coordIndex.setValues(0, len(pts), range(len(pts)))


# Alias for compatibility with v0.18 and earlier
ViewProviderDraftLabel = ViewProviderLabel

## @}
