# Copyright (c) 2021, NVIDIA CORPORATION.  All rights reserved.
#
# NVIDIA CORPORATION and its licensors retain all intellectual property
# and proprietary rights in and to this software, related documentation
# and any modifications thereto.  Any use, reproduction, disclosure or
# distribution of this software and related documentation without an express
# license agreement from NVIDIA CORPORATION is strictly prohibited.
#

from __future__ import annotations

from collections import defaultdict
from dataclasses import dataclass
from typing import TYPE_CHECKING, List, Sequence, Tuple

if TYPE_CHECKING:
    from .manipulator import TransformManipulator

import copy
import math
from functools import lru_cache

import carb.input
import carb.settings
import numpy as np
from omni.ui import color as cl
from omni.ui import scene as sc

from .settings_constants import c as CONSTANTS


@lru_cache()
def __get_input() -> carb.input.IInput:
    return carb.input.acquire_input_interface()


def _is_alt_down() -> bool:
    input = __get_input()
    return (
        input.get_keyboard_value(None, carb.input.KeyboardInput.LEFT_ALT)
        + input.get_keyboard_value(None, carb.input.KeyboardInput.RIGHT_ALT)
        > 0
    )


class PreventOthers(sc.GestureManager):
    """
    Manager makes TransformGesture the priority gesture
    """

    def can_be_prevented(self, gesture):
        # Never prevent in the middle or at the end of drag
        return (
            gesture.state != sc.GestureState.CHANGED
            and gesture.state != sc.GestureState.ENDED
            and gesture.state != sc.GestureState.CANCELED
        )

    def should_prevent(self, gesture, preventer):
        if (
            (isinstance(preventer, TransformGesture) or isinstance(preventer, DummyClickGesture))
            and (preventer.state == sc.GestureState.BEGAN or preventer.state == sc.GestureState.CHANGED)
            and not _is_alt_down()  # don't prevent other gesture if alt (camera manip) is down
        ):
            if isinstance(gesture, TransformGesture):
                if gesture.order > preventer.order:
                    return True

                elif gesture.order == preventer.order:
                    # Transform vs Transform depth test
                    return gesture.gesture_payload.ray_distance > preventer.gesture_payload.ray_distance
            else:
                # Transform is the priority when it's against any other gesture
                return True
        return super().should_prevent(gesture, preventer)


class HoverDepthTest(sc.GestureManager):
    """
    Manager that is doing depth test for hover gestures
    """

    def can_be_prevented(self, gesture):
        return isinstance(gesture, HighlightGesture)

    def should_prevent(self, gesture, preventer):
        if (
            isinstance(gesture, HighlightGesture) and not _is_alt_down()
        ):  # don't prevent other gesture if alt (camera manip) is down
            if isinstance(preventer, HighlightGesture):
                if preventer.state == sc.GestureState.BEGAN or preventer.state == sc.GestureState.CHANGED:
                    if gesture.order > preventer.order:
                        return True

                    elif gesture.order == preventer.order:
                        # Hover vs Hover depth test
                        return gesture.gesture_payload.ray_distance > preventer.gesture_payload.ray_distance
        return False


class TransformDragGesturePayload(sc.AbstractGesture.GesturePayload):
    def __init__(self, base: sc.AbstractGesture.GesturePayload, changing_item: sc.AbstractManipulatorItem):
        super().__init__(base.item_closest_point, base.ray_closest_point, base.ray_distance)
        self.changing_item = changing_item


@dataclass(init=False)
class TranslateDragGesturePayload(TransformDragGesturePayload):
    """
    Payload this class has:
        - axis (float3): The axis along which the translation happened.
        - moved_delta (float3): Moved delta since last change for each axis.
        - moved (float3): Total moved distance since beginning of the drag.
    """

    axis: Sequence[float]
    moved_delta: Sequence[float]
    moved: Sequence[float]


@dataclass(init=False)
class RotateDragGesturePayload(TransformDragGesturePayload):
    """
    Payload this class has:
        - axis (float3): The axis around which the rotation happened.
        - angle_delta (float): Rotated delta since last change.
        - angle (float): Total angle rotated distance since beginning of the drag.
        - screen_space (bool): If the rotation happened in screen space.
        - free_rotation (bool): If the rotation is a free rotation (dragging on the center sphere).
    """

    axis: Sequence[float]
    angle_delta: float
    angle: float
    screen_space: bool
    free_rotation: bool


@dataclass(init=False)
class ScaleDragGesturePayload(TransformDragGesturePayload):
    """
    Payload this class has:
        - axis (float3): The axis along which the scaling happened.
        - scale (float3): Scaled value on each axis.
    """

    axis: Sequence[float]
    scale: Sequence[float]


class TransformChangedGesture(sc.ManipulatorGesture):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)

    def process(self):
        if not self.gesture_payload:
            return

        # Redirection to methods
        if self.state == sc.GestureState.BEGAN:
            self.on_began()
        elif self.state == sc.GestureState.CHANGED:
            self.on_changed()
        elif self.state == sc.GestureState.ENDED:
            self.on_ended()
        elif self.state == sc.GestureState.CANCELED:
            self.on_canceled()

    # Public API:
    def on_began(self):
        ...

    def on_changed(self):
        ...

    def on_ended(self):
        ...

    def on_canceled(self):
        ...


class TranslateChangedGesture(TransformChangedGesture):
    ...


class RotateChangedGesture(TransformChangedGesture):
    ...


class ScaleChangedGesture(TransformChangedGesture):
    ...


####################
# Internal gestures
####################


class HighlightControl:

    # Use static member to track across ALL HighlightControl instances,
    # because more than one HighlightControl can highlight the same widget (e.g. translate center and plane quads)
    __status_tracker = defaultdict(int)
    __original_colors = {}

    def __init__(self, items=None, color=None):
        self._items = items
        # From rendering\source\plugins\carb.imguizmo\ImGuizmo.cpp
        # static const ImU32 selectionColor = 0x8A1CE6E6;
        self._selection_color = color if color else cl("#e6e61c8a")

    def highlight(self, sender):
        if self._items is None:
            HighlightControl.__status_tracker[sender] += 1
            if HighlightControl.__status_tracker[sender] == 1:
                self._highlight(sender)
        else:
            for item in self._items:
                HighlightControl.__status_tracker[item] += 1
                if HighlightControl.__status_tracker[item] == 1:
                    self._highlight(item)

    def dehighlight(self, sender):
        if self._items is None:
            HighlightControl.__status_tracker[sender] -= 1
            if HighlightControl.__status_tracker[sender] <= 0:
                self._dehighlight(sender)
                HighlightControl.__status_tracker.pop(sender)
        else:
            for item in self._items:
                HighlightControl.__status_tracker[item] -= 1
                if HighlightControl.__status_tracker[item] <= 0:
                    self._dehighlight(item)
                    HighlightControl.__status_tracker.pop(item)

    def _highlight(self, item):
        if hasattr(item, "color"):
            HighlightControl.__original_colors[item] = item.color
            item.color = self._selection_color

        if hasattr(item, "colors"):
            HighlightControl.__original_colors[item] = item.colors.copy()
            colors = item.colors.copy()
            for i in range(len(colors)):
                colors[i] = self._selection_color
            item.colors = colors

    def _dehighlight(self, item):
        if hasattr(item, "color"):
            item.color = HighlightControl.__original_colors[item]

        if hasattr(item, "colors"):
            item.colors = HighlightControl.__original_colors[item]


class HighlightGesture(sc.HoverGesture):
    def __init__(self, highlight_ctrl: HighlightControl, order: int = 0):
        super().__init__(manager=HoverDepthTest())
        self._highlight_ctrl = highlight_ctrl
        self._begun = False
        self._order = order

    @property
    def order(self) -> int:
        return self._order

    def process(self):
        if isinstance(self.sender, sc.Arc) and self.sender.gesture_payload.culled:
            # Turn down the gesture if it on the wrong side of the arc
            if self.state == sc.GestureState.BEGAN:
                self.state = sc.GestureState.POSSIBLE
            if self.state == sc.GestureState.CHANGED:
                self.state = sc.GestureState.ENDED

        if _is_alt_down():
            # Turn down the gesture if manipulating camera
            # If gesture already began, don't turn it down as ALT won't trigger camera manipulation at this point
            if self.state == sc.GestureState.BEGAN:
                self.state = sc.GestureState.POSSIBLE

        if self.state == sc.GestureState.BEGAN:
            self._on_began()
        elif self.state == sc.GestureState.ENDED:
            self._on_ended()
        elif self.state == sc.GestureState.CANCELED:
            self._on_canceled()
        elif self.state == sc.GestureState.CHANGED:
            self._on_changed()
        elif self.state == sc.GestureState.PREVENTED:
            self._on_prevented()
        super().process()

    def _on_began(self):
        self._begun = True
        self._highlight_ctrl.highlight(self.sender)

    def _on_ended(self):
        if self._begun:
            self._begun = False
            self._highlight_ctrl.dehighlight(self.sender)

    def _on_changed(self):
        ...

    def _on_canceled(self):
        self._on_ended()

    def _on_prevented(self):
        self._on_ended()


class TransformGesture(sc.DragGesture):
    def __init__(
        self, manipulator: TransformManipulator, highlight_ctrl: HighlightControl, order: int = 0, *args, **kwargs
    ):
        super().__init__(*args, **kwargs)
        self._manipulator = manipulator
        self._highlight_ctrl = highlight_ctrl
        self._order = order
        self._toolbar_was_visible = None
        self._began = False

    @property
    def order(self) -> int:
        return self._order

    def process(self):
        if _is_alt_down():
            # Turn down the gesture if manipulating camera
            # If gesture already began, don't turn it down as ALT won't trigger camera manipulation at this point
            if self.state == sc.GestureState.BEGAN:
                self.state = sc.GestureState.POSSIBLE

        if self.state == sc.GestureState.BEGAN:
            self._on_began()
        elif self.state == sc.GestureState.ENDED:
            self._on_ended()
        elif self.state == sc.GestureState.CANCELED:
            self._on_canceled()
        elif self.state == sc.GestureState.CHANGED:
            self._on_changed()
        elif self.state == sc.GestureState.PREVENTED:
            self._on_prevented()
        super().process()

    def _on_began(self):
        self._began = True

        self._toolbar_was_visible = self._manipulator.toolbar_visible
        self._manipulator.toolbar_visible = False

    def _on_ended(self):
        if self._began and self._toolbar_was_visible is not None:
            self._manipulator.toolbar_visible = self._toolbar_was_visible
            self._toolbar_was_visible = None
        self._began = False

    def _on_changed(self):
        ...

    def _on_canceled(self):
        if self._began:
            self._on_ended()

            if self._toolbar_was_visible is not None:
                self._manipulator.toolbar_visible = self._toolbar_was_visible
                self._toolbar_was_visible = None

    def _on_prevented(self):
        if self._began:
            self._on_ended()


class TranslateGesture(TransformGesture):
    def __init__(self, manipulator: TransformManipulator, axis, highlight_ctrl: HighlightControl, order: int = 0):
        super().__init__(manipulator=manipulator, highlight_ctrl=highlight_ctrl, order=order, manager=PreventOthers())
        self._axis = axis

    def _on_began(self):
        super()._on_began()

        self._moved = [0, 0, 0]  # moved since begin edit, non-snapped
        self._moved_snap = [0, 0, 0]
        self._changing_item = self._manipulator.model.get_item("translate")

        payload = TranslateDragGesturePayload(self.gesture_payload, self._changing_item)
        payload.axis = self._axis
        self._manipulator._process_gesture(TranslateChangedGesture, self.state, payload)
        if self._highlight_ctrl:
            self._highlight_ctrl.highlight(self.sender)

    def _on_changed(self):
        super()._on_changed()

        moved_delta = self.sender.gesture_payload.moved
        moved_delta = self._manipulator._transform.transform_space(
            sc.Space.WORLD, sc.Space.OBJECT, [moved_delta[0], moved_delta[1], moved_delta[2], 0]
        )
        moved_delta = moved_delta[0:3]
        moved_prev = self._moved_snap[:]
        self._moved = [sum(x) for x in zip(self._moved, moved_delta)]

        self._moved_snap = self._moved.copy()
        snap = self._manipulator.model.get_snap(self._changing_item)
        if snap is not None:
            for i, snap_axis in enumerate(snap):
                if snap_axis:
                    self._moved_snap[i] = math.copysign(abs(self._moved[i]) // snap_axis * snap_axis, self._moved[i])
            moved_delta = [m - mp for m, mp in zip(self._moved_snap, moved_prev)]

        if moved_delta != [0, 0, 0]:
            new_gesture_payload = TranslateDragGesturePayload(self.gesture_payload, self._changing_item)
            new_gesture_payload.axis = self._axis
            new_gesture_payload.moved_delta = moved_delta
            new_gesture_payload.moved = self._moved_snap

            self._manipulator._process_gesture(TranslateChangedGesture, self.state, new_gesture_payload)

    def _on_ended(self):
        super()._on_ended()

        payload = TranslateDragGesturePayload(self.gesture_payload, self._changing_item)
        payload.axis = self._axis
        self._manipulator._process_gesture(TranslateChangedGesture, self.state, payload)
        if self._highlight_ctrl:
            self._highlight_ctrl.dehighlight(self.sender)

    def _on_canceled(self):
        self._on_ended()


class RotationGesture(TransformGesture):
    def __init__(
        self, manipulator: TransformManipulator, axis, viz_arc, highlight_ctrl: HighlightControl, order: int = 0
    ):
        super().__init__(manipulator=manipulator, highlight_ctrl=highlight_ctrl, order=order, manager=PreventOthers())
        self._viz_arc = viz_arc
        self._axis = axis
        self._angle = 0
        self._last_free_intersect = None
        self._settings = carb.settings.get_settings()

    def process(self):
        if isinstance(self.sender, sc.Arc) and self.sender.gesture_payload.culled:
            # Turn down the gesture if it on the wrong side of the arc
            if self.state == sc.GestureState.BEGAN:
                self.state = sc.GestureState.POSSIBLE

        super().process()

    def _on_began(self):
        super()._on_began()

        self._original_tickness = self.sender.thickness
        self.sender.thickness = self._original_tickness + 1
        self._begin_angle = self.sender.gesture_payload.angle

        if self._viz_arc:
            self._viz_arc.begin = self._begin_angle

        if self._axis is None:
            self._last_free_intersect, _ = self._sphere_intersect()

        self._changing_item = self._manipulator.model.get_item("rotate")
        self._manipulator._process_gesture(
            RotateChangedGesture, self.state, RotateDragGesturePayload(self.gesture_payload, self._changing_item)
        )
        if self._highlight_ctrl:
            self._highlight_ctrl.highlight(self.sender)

    def _on_changed(self):
        super()._on_changed()

        prev_angle = self._angle
        self._angle = self.sender.gesture_payload.angle - self._begin_angle
        self._angle = self._angle / math.pi * 180  # convert to degree

        snap = self._manipulator.model.get_snap(self._changing_item)
        if snap:  # Not None and not zero
            self._angle = self._angle // snap * snap
            angle_delta = self._angle - prev_angle
        else:
            angle_delta = self.sender.gesture_payload.moved_angle
            angle_delta = angle_delta / math.pi * 180  # convert to degree

        screen_space = False
        free_rotation = False

        # handle the free rotation mode when grabbing the center of rotation manipulator
        if self._axis is None:
            axis, angle_delta = self._handle_free_rotation()
            if axis is not None and angle_delta is not None:
                free_rotation = True
            else:
                # early return, no gesture is generated for invalid free rotation
                return
        elif self._axis == [0, 0, 0]:
            axis = self.sender.transform_space(sc.Space.OBJECT, sc.Space.WORLD, [0, 0, 1, 0])
            screen_space = True
        else:
            axis = self._axis

        # normalize
        axis_len = math.sqrt(axis[0] ** 2 + axis[1] ** 2 + axis[2] ** 2)
        axis = [v / axis_len for v in axis]

        if not free_rotation:
            # confine angle in [-180, 180) range so the overlay doesn't block manipulated object too much
            angle = (self._angle - 360.0 * math.floor(self._angle / 360.0 + 0.5)) / 180.0 * math.pi
            self._viz_arc.end = angle + self._viz_arc.begin
            self._viz_arc.visible = True

        if angle_delta:
            new_gesture_payload = RotateDragGesturePayload(self.gesture_payload, self._changing_item)
            new_gesture_payload.axis = axis
            new_gesture_payload.angle = self._angle
            new_gesture_payload.angle_delta = angle_delta
            new_gesture_payload.screen_space = screen_space
            new_gesture_payload.free_rotation = free_rotation

            self._manipulator._process_gesture(RotateChangedGesture, self.state, new_gesture_payload)

    def _on_ended(self):
        super()._on_ended()

        self.sender.thickness = self._original_tickness

        if self._viz_arc:
            self._viz_arc.visible = False

        self._manipulator._process_gesture(
            RotateChangedGesture, self.state, RotateDragGesturePayload(self.gesture_payload, self._changing_item)
        )
        if self._highlight_ctrl:
            self._highlight_ctrl.dehighlight(self.sender)

        if self._axis is None:
            self._last_free_intersect = None

    def _on_canceled(self):
        self._on_ended()

    def _sphere_intersect(self) -> Tuple[List[float], bool]:
        intersect = self.sender.gesture_payload.ray_closest_point
        ndc_location = self.sender.transform_space(sc.Space.WORLD, sc.Space.NDC, intersect)
        ndc_ray_origin = copy.copy(ndc_location)
        ndc_ray_origin[2] = 0.0

        object_ray_origin = np.array(self.sender.transform_space(sc.Space.NDC, sc.Space.OBJECT, ndc_ray_origin))
        object_ray_end = np.array(self.sender.transform_space(sc.Space.NDC, sc.Space.OBJECT, ndc_location))

        dir = object_ray_end - object_ray_origin
        dir = dir / np.linalg.norm(dir)

        a = np.dot(dir, dir)
        b = 2.0 * np.dot(object_ray_origin, dir)
        c = np.dot(object_ray_origin, object_ray_origin) - self.sender.radius * self.sender.radius

        discriminant = b * b - 4 * a * c
        if discriminant >= 0.0:
            sqrt_discriminant = math.sqrt(discriminant)
            t = (-b - sqrt_discriminant) / (2.0 * a)
            if t >= 0:
                point = object_ray_origin + t * dir
                world = self.sender.transform_space(sc.Space.OBJECT, sc.Space.WORLD, point.tolist())
                return world, True

        # If no intersection, get the neareast point on the ray to the sphere
        nearest_dist = np.dot(-object_ray_origin, dir)
        point = object_ray_origin + dir * nearest_dist
        world = self.sender.transform_space(sc.Space.OBJECT, sc.Space.WORLD, point.tolist())
        return world, False

    def _handle_free_rotation(self) -> Tuple[List[float], float]:
        with np.errstate(all="raise"):
            try:
                axis = None
                angle_delta = None

                nearest_ray_point, intersected = self._sphere_intersect()
                last_intersect = copy.copy(self._last_free_intersect)
                self._last_free_intersect = nearest_ray_point

                mode = self._settings.get(CONSTANTS.FREE_ROTATION_TYPE_SETTING)
                if intersected or mode == CONSTANTS.FREE_ROTATION_TYPE_CLAMPED:
                    # Use clamped mode as long as ray intersects with sphere to maintain consistent behavior when dragging within the sphere
                    # clamped at the edge of free rotation sphere
                    if nearest_ray_point is not None and last_intersect is not None:
                        # calculate everything in world space
                        origin = np.array(self.sender.transform_space(sc.Space.OBJECT, sc.Space.WORLD, [0, 0, 0]))
                        vec1 = last_intersect - origin
                        vec1 = vec1 / np.linalg.norm(vec1)
                        vec2 = nearest_ray_point - origin
                        vec2 = vec2 / np.linalg.norm(vec2)

                        axis = np.cross(vec1, vec2)
                        mag = np.linalg.norm(axis)
                        if mag > 0:
                            axis = axis / mag
                            dot = np.dot(vec1, vec2)
                            if dot >= -1 and dot <= 1:
                                angle_delta = np.arccos(dot) / math.pi * 180
                                if not math.isfinite(angle_delta):
                                    raise FloatingPointError()

                elif mode == CONSTANTS.FREE_ROTATION_TYPE_CONTINUOUS:
                    # Continuous mode. Only fallback to it if not intersected and continuous_mode is on.
                    # extend beyond sphere edge for free rotation based on moved distance
                    moved = np.array(self.sender.gesture_payload.moved)
                    forward = self.sender.transform_space(sc.Space.OBJECT, sc.Space.WORLD, [0, 0, 1, 0])
                    forward = np.array([forward[0], forward[1], forward[2]])

                    moved_len = math.sqrt(np.dot(moved, moved))
                    if moved_len > 0:
                        moved_dir = moved / moved_len
                        forward = forward / np.linalg.norm(forward)
                        axis = np.cross(forward, moved_dir).tolist()
                        angle_delta = moved_len
            except Exception:
                axis = None
                angle_delta = None
            finally:
                return axis, angle_delta


class ScaleGesture(TransformGesture):
    def __init__(
        self,
        manipulator: TransformManipulator,
        axis,
        highlight_ctrl: HighlightControl,
        handle_lines=[],
        handle_dots=[],
        order: int = 0,
    ):
        super().__init__(manipulator=manipulator, highlight_ctrl=highlight_ctrl, order=order, manager=PreventOthers())
        self._axis = axis
        self._handle_lines = handle_lines
        self._handle_dots = handle_dots

    def _get_dir_and_length_from_origin(self, point):
        point = self.sender.transform_space(
            sc.Space.WORLD, sc.Space.OBJECT, self.sender.gesture_payload.item_closest_point
        )
        transform = self._manipulator.model.get_as_floats(self._manipulator.model.get_item("transform"))
        origin_in_local = self.sender.transform_space(
            sc.Space.WORLD, sc.Space.OBJECT, [transform[12], transform[13], transform[14]]
        )
        diff = [s - o for s, o in zip(point, origin_in_local)]
        length = math.sqrt(diff[0] ** 2 + diff[1] ** 2 + diff[2] ** 2)
        dir = [x / length for x in diff]

        return point, dir, length

    def _on_began(self):
        super()._on_began()

        self._original_ends = [line.end.copy() for line in self._handle_lines]
        self._original_dot_transform = [
            [dot.transform[12], dot.transform[13], dot.transform[14]] for dot in self._handle_dots
        ]
        self._start_point, self._direction, self._start_length = self._get_dir_and_length_from_origin(
            self.sender.gesture_payload.item_closest_point
        )
        self._accumulated_distance = self._start_length
        self._changing_item = self._manipulator.model.get_item("scale")
        self._scale_prev = None
        self._manipulator._process_gesture(
            ScaleChangedGesture, self.state, ScaleDragGesturePayload(self.gesture_payload, self._changing_item)
        )
        if self._highlight_ctrl:
            self._highlight_ctrl.highlight(self.sender)

        self._first_change = False

    def _on_changed(self):
        super()._on_changed()

        if isinstance(self.sender, sc.Arc) and self._first_change is False:
            # handle the omni scale (center point) differently as the start point is very close to origin
            # so we do it when it has enough distance
            if self.sender.gesture_payload.distance_to_center > 10:
                start_point, direction, length = self._get_dir_and_length_from_origin(
                    self.sender.gesture_payload.ray_closest_point
                )
                self._start_point = start_point
                self._direction = direction
                self._accumulated_distance = self._start_length = length * 5  # make the step smaller
                self._first_change = True
            else:
                # early return. don't start change before we can determine those values
                return

        # use object space delta
        moved = self.sender.gesture_payload.moved
        moved = self.sender.transform_space(sc.Space.WORLD, sc.Space.OBJECT, [moved[0], moved[1], moved[2], 0])
        distance = moved[0] * self._direction[0] + moved[1] * self._direction[1] + moved[2] * self._direction[2]
        self._accumulated_distance += distance

        if self._accumulated_distance == 0:
            self._accumulated_distance = 0.001

        scale = self._accumulated_distance / self._start_length
        snap = self._manipulator.model.get_snap(self._changing_item)
        if snap:  # Not None and not zero
            if abs(scale) < snap:
                scale = math.copysign(1, scale)
            else:
                snap_scale = (abs(scale) - 1) // snap * snap + 1
                scale = math.copysign(snap_scale, scale)

        for i, line in enumerate(self._handle_lines):
            line.end = [
                self._original_ends[i][0] * scale,
                self._original_ends[i][1] * scale,
                self._original_ends[i][2] * scale,
            ]

        for i, dot in enumerate(self._handle_dots):
            t = dot.transform
            t[12] = self._original_dot_transform[i][0] * scale
            t[13] = self._original_dot_transform[i][1] * scale
            t[14] = self._original_dot_transform[i][2] * scale

            dot.transform = t

        if scale != self._scale_prev:
            self._scale_prev = scale
            new_gesture_payload = ScaleDragGesturePayload(self.gesture_payload, self._changing_item)
            new_gesture_payload.axis = self._axis
            new_gesture_payload.scale = scale
            self._manipulator._process_gesture(ScaleChangedGesture, self.state, new_gesture_payload)

    def _on_ended(self):
        super()._on_ended()

        for i, line in enumerate(self._handle_lines):
            line.end = self._original_ends[i]

        for i, dot in enumerate(self._handle_dots):
            dot.transform[12] = self._original_dot_transform[i][0]
            dot.transform[13] = self._original_dot_transform[i][1]
            dot.transform[14] = self._original_dot_transform[i][2]

        self._manipulator._process_gesture(
            ScaleChangedGesture, self.state, ScaleDragGesturePayload(self.gesture_payload, self._changing_item)
        )
        if self._highlight_ctrl:
            self._highlight_ctrl.dehighlight(self.sender)

    def _on_canceled(self):
        self._on_ended()


# The sole purpose of this dummy gesture is to prevent further viewport drag interaction by emitting a TransformChangedGesture,
# which:
#    Turns of selection rect in VP1
#    Prevents other gestures in VP2
class DummyGesture(TransformGesture):
    def __init__(self, manipulator: TransformManipulator):
        # toolbar always has smaller order value to take precedence over manipulator handles.
        super().__init__(manipulator, None, order=-999, manager=PreventOthers())

    def _on_began(self):
        self._manipulator._process_gesture(
            TransformChangedGesture, self.state, TransformDragGesturePayload(self.gesture_payload, None)
        )

    def _on_changed(self):
        ...

    def _on_ended(self):
        self._manipulator._process_gesture(
            TransformChangedGesture, self.state, TransformDragGesturePayload(self.gesture_payload, None)
        )

    def _on_canceled(self):
        self._on_ended()


# The sole purpose of this dummy gesture is to prevent further viewport click interaction which:
#    Prevents other gestures in VP2 (e.g. right click on manipulator toolbar won't further trigger viewport context menu)
#    (no VP1 support for this!)
class DummyClickGesture(sc.ClickGesture):
    def __init__(self, mouse_button: int):
        super().__init__(mouse_button=mouse_button, manager=PreventOthers())

    @property
    def order(self):
        return -1000
