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

# ***************************************************************************
# *   (c) 2009 Yorik van Havre <yorik@uncreated.net>                        *
# *   (c) 2010 Ken Cline <cline@frii.com>                                   *
# *   (c) 2020 Eliud Cabrera Castillo <e.cabrera-castillo@tum.de>           *
# *                                                                         *
# *   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 utility functions that are used by many Draft Gui Commands.

These functions are used by different command classes in the `DraftTools`
module. We assume that the graphical interface was already loaded
as they operate on selections and graphical properties.
"""
## @package gui_tool_utils
# \ingroup draftguitools
# \brief Provides utility functions that are used by many Draft Gui Commands.

## \addtogroup draftguitools
# @{
import re

import FreeCAD as App
import FreeCADGui as Gui
import WorkingPlane
from draftutils import gui_utils
from draftutils import params
from draftutils import utils
from draftutils.messages import _wrn
from draftutils.translate import translate


# Set modifier keys from the parameter database
MODS = ["shift", "ctrl", "alt"]


def get_mod_constrain_key():
    return MODS[params.get_param("modconstrain")]


def get_mod_snap_key():
    return MODS[params.get_param("modsnap")]


def get_mod_alt_key():
    return MODS[params.get_param("modalt")]


_HINT_MOD_KEYS = [Gui.UserInput.KeyShift, Gui.UserInput.KeyControl, Gui.UserInput.KeyAlt]


# To allows for easy concatenation the _get_hint_* functions
# always return a list (with a single item or an empty list).


def _get_hint_mod_constrain():
    key = _HINT_MOD_KEYS[params.get_param("modconstrain")]
    return [Gui.InputHint(translate("draft", "%1 constrain"), key)]


def _get_hint_mod_snap():
    if params.get_param("alwaysSnap"):
        return []
    key = _HINT_MOD_KEYS[params.get_param("modsnap")]
    return [Gui.InputHint(translate("draft", "%1 snap"), key)]


def _get_hint_xyz_constrain():
    pattern = re.compile("[A-Z]")
    shortcut_x = params.get_param("inCommandShortcutRestrictX").upper()
    shortcut_y = params.get_param("inCommandShortcutRestrictY").upper()
    shortcut_z = params.get_param("inCommandShortcutRestrictZ").upper()
    if (
        pattern.fullmatch(shortcut_x)
        and pattern.fullmatch(shortcut_y)
        and pattern.fullmatch(shortcut_z)
    ):
        key_x = getattr(Gui.UserInput, "Key" + shortcut_x)
        key_y = getattr(Gui.UserInput, "Key" + shortcut_y)
        key_z = getattr(Gui.UserInput, "Key" + shortcut_z)
        return [
            Gui.InputHint(translate("draft", "%1/%2/%3 switch constraint"), key_x, key_y, key_z)
        ]
    return []


def _get_hint_relative():
    pattern = re.compile("[A-Z]")
    shortcut = params.get_param("inCommandShortcutRelative").upper()
    if pattern.fullmatch(shortcut):
        key = getattr(Gui.UserInput, "Key" + shortcut)
        return [Gui.InputHint(translate("draft", "%1 toggle relative"), key)]
    return []


def _get_hint_global():
    pattern = re.compile("[A-Z]")
    shortcut = params.get_param("inCommandShortcutGlobal").upper()
    if pattern.fullmatch(shortcut):
        key = getattr(Gui.UserInput, "Key" + shortcut)
        return [Gui.InputHint(translate("draft", "%1 toggle global"), key)]
    return []


def _get_hint_continue():
    pattern = re.compile("[A-Z]")
    shortcut = params.get_param("inCommandShortcutContinue").upper()
    if pattern.fullmatch(shortcut):
        key = getattr(Gui.UserInput, "Key" + shortcut)
        return [Gui.InputHint(translate("draft", "%1 toggle continue"), key)]
    return []


def format_unit(exp, unit="mm"):
    """Return a formatting string to set a number to the correct unit."""
    return App.Units.Quantity(exp, App.Units.Length).UserString


formatUnit = format_unit


def select_object(arg):
    """Handle the selection of objects depending on buttons pressed.

    This is a scene event handler, to be called from the Draft tools
    when they need to select an object.
    ::
        self.call = self.view.addEventCallback("SoEvent", select_object)

    Parameters
    ----------
    arg: Coin event
        The Coin event received from the 3D view.

        If it is of type Keyboard and the `ESCAPE` key, it runs the `finish`
        method of the active command.

        If Ctrl key is pressed, multiple selection is enabled until the
        button is released.
        Then it runs the `proceed` method of the active command
        to continue with the command's logic.
    """
    if arg["Type"] == "SoKeyboardEvent":
        if arg["Key"] == "ESCAPE":
            App.activeDraftCommand.finish()
            # TODO: this part raises a coin3D warning about scene traversal.
            # It needs to be fixed.
    elif not arg["CtrlDown"] and Gui.Selection.hasSelection():
        App.activeDraftCommand.proceed()


selectObject = select_object


def has_mod(args, mod):
    """Check if args has a specific modifier.

    Parameters
    ----------
    args: Coin event
        The Coin event received from the 3D view.

    mod: str
        A string indicating the modifier, either `'shift'`, `'ctrl'`,
        or `'alt'`.

    Returns
    -------
    bool
        It returns `args["ShiftDown"]`, `args["CtrlDown"]`,
        or `args["AltDown"]`, depending on the passed value of `mod`.
    """
    if mod == "shift":
        return args["ShiftDown"]
    elif mod == "ctrl":
        return args["CtrlDown"]
    elif mod == "alt":
        return args["AltDown"]


hasMod = has_mod


def set_mod(args, mod, state):
    """Set a specific modifier state in args.

    Parameters
    ----------
    args: Coin event
        The Coin event received from the 3D view.

    mod: str
        A string indicating the modifier, either `'shift'`, `'ctrl'`,
        or `'alt'`.

    state: bool
        The boolean value of `state` is assigned to `args["ShiftDown"]`,
        `args["CtrlDown"]`, or `args["AltDown"]`
        depending on `mod`.
    """
    if mod == "shift":
        args["ShiftDown"] = state
    elif mod == "ctrl":
        args["CtrlDown"] = state
    elif mod == "alt":
        args["AltDown"] = state


setMod = set_mod


def get_point(target, args, noTracker=False):
    """Return a constrained 3D point and its original point.

    It is used by the Draft tools.

    Parameters
    ----------
    target: object (class)
        The target object with a `node` attribute. If this is present,
        return the last node, otherwise return `None`.

        In the Draft tools, `target` is essentially the same class
        of the Gui command, that is, `self`. Therefore, this method
        probably makes more sense as a class method.

    args: Coin event
        The Coin event received from the 3D view.

    noTracker: bool, optional
        It defaults to `False`.
        If it is `True`, the tracking line will not be displayed.

    Returns
    -------
    CoinPoint, Base::Vector3, str
        It returns a tuple with some information.

        The first is the Coin point returned by `Snapper.snap`
        or by the `ActiveView`; the second is that same point
        turned into an `App.Vector`,
        and the third is some information of the point
        returned by the `Snapper` or by the `ActiveView`.
    """
    ui = Gui.draftToolBar
    if not ui.mouse:
        return None, None, None

    if target.node:
        last = target.node[-1]
    else:
        last = None

    smod = has_mod(args, get_mod_snap_key())
    cmod = has_mod(args, get_mod_constrain_key())
    point = None

    if hasattr(Gui, "Snapper"):
        point = Gui.Snapper.snap(
            args["Position"], lastpoint=last, active=smod, constrain=cmod, noTracker=noTracker
        )
        info = Gui.Snapper.snapInfo
        mask = Gui.Snapper.affinity
    if not point:
        p = Gui.ActiveDocument.ActiveView.getCursorPos()
        point = Gui.ActiveDocument.ActiveView.getPoint(p)
        info = Gui.ActiveDocument.ActiveView.getObjectInfo(p)
        mask = None

    ctrlPoint = App.Vector(point)
    wp = WorkingPlane.get_working_plane(update=False)
    if target.node:
        if target.featureName == "Rectangle":
            ui.displayPoint(point, target.node[0], plane=wp, mask=mask)
        else:
            ui.displayPoint(point, target.node[-1], plane=wp, mask=mask)
    else:
        ui.displayPoint(point, plane=wp, mask=mask)
    return point, ctrlPoint, info


getPoint = get_point


def set_working_plane_to_object_under_cursor(mouseEvent):
    """Align the working plane to the face under the cursor.

    The working plane is only aligned if it is `'auto'`.

    Parameters
    ----------
    mouseEvent: Coin event
        Coin mouse event.

    Returns
    -------
    App::DocumentObject or None
        The parent object the face belongs to, if alignment occurred, or None.
    """
    objectUnderCursor = gui_utils.get_3d_view().getObjectInfo(
        (mouseEvent["Position"][0], mouseEvent["Position"][1])
    )

    if not objectUnderCursor:
        return None
    if "Face" not in objectUnderCursor["Component"]:
        return None
    wp = WorkingPlane.get_working_plane(update=False)
    if not wp.auto:
        return None

    import Part

    if "ParentObject" in objectUnderCursor:
        obj = objectUnderCursor["ParentObject"]
        sub = objectUnderCursor["SubName"]
    else:
        obj = App.ActiveDocument.getObject(objectUnderCursor["Object"])
        sub = objectUnderCursor["Component"]
    shape = Part.getShape(obj, sub, needSubElement=True, retType=0)

    if wp.align_to_face(shape, _hist_add=False):
        wp.auto = True
        return obj

    return None


setWorkingPlaneToObjectUnderCursor = set_working_plane_to_object_under_cursor


def set_working_plane_to_selected_object():
    """Align the working plane to a preselected face.

    The working plane is only aligned if it is `'auto'`.

    Returns
    -------
    App::DocumentObject or None
        The parent object the face belongs to, if alignment occurred, or None.
    """
    wp = WorkingPlane.get_working_plane(update=False)
    if not wp.auto:
        return None

    sels = Gui.Selection.getSelectionEx("", 0)

    if (
        len(sels) == 1
        and len(sels[0].SubObjects) == 1
        and sels[0].SubObjects[0].ShapeType == "Face"
    ):
        import Part

        shape = Part.getShape(
            sels[0].Object, sels[0].SubElementNames[0], needSubElement=True, retType=0
        )

        if wp.align_to_face(shape, _hist_add=False):
            wp.auto = True
            return sels[0].Object

    return None


setWorkingPlaneToSelectedObject = set_working_plane_to_selected_object


def get_support(mouseEvent=None):
    """ "Align the working plane to a preselected face or the face under the cursor.

    The working plane is only aligned if it is `'auto'`.

    Parameters
    ----------
    mouseEvent: Coin event, optional
        Defaults to `None`.
        Coin mouse event.

    Returns
    -------
    App::DocumentObject or None
        The parent object the face belongs to, if alignment occurred, or None.
    """
    if mouseEvent is None:
        return set_working_plane_to_selected_object()
    return set_working_plane_to_object_under_cursor(mouseEvent)


getSupport = get_support


def redraw_3d_view():
    """Force a redraw of 3D view or do nothing if it fails."""
    try:
        Gui.ActiveDocument.ActiveView.redraw()
    except AttributeError as err:
        _wrn(err)


redraw3DView = redraw_3d_view

## @}
