# Copyright (c) 2020-2021, <NAME>
# License: MIT License
from typing import TYPE_CHECKING, Tuple
import math
from ezdxf.math import Vec3, X_AXIS, Y_AXIS, Vec2, Matrix44, sign, OCS

if TYPE_CHECKING:
    from ezdxf.eztypes import DXFGraphic, Vertex

__all__ = [
    "TransformError",
    "NonUniformScalingError",
    "InsertTransformationError",
    "transform_extrusion",
    "transform_thickness_and_extrusion_without_ocs",
    "OCSTransform",
]


class TransformError(Exception):
    pass


class NonUniformScalingError(TransformError):
    pass


class InsertTransformationError(TransformError):
    pass


def transform_thickness_and_extrusion_without_ocs(
    entity: "DXFGraphic", m: Matrix44
) -> None:
    if entity.dxf.hasattr("thickness"):
        thickness = entity.dxf.thickness
        reflection = sign(thickness)
        thickness = m.transform_direction(entity.dxf.extrusion * thickness)
        entity.dxf.thickness = thickness.magnitude * reflection
        entity.dxf.extrusion = thickness.normalize()
    elif entity.dxf.hasattr("extrusion"):  # without thickness?
        extrusion = m.transform_direction(entity.dxf.extrusion)
        entity.dxf.extrusion = extrusion.normalize()


def transform_extrusion(extrusion: "Vertex", m: Matrix44) -> Tuple[Vec3, bool]:
    """Transforms the old `extrusion` vector into a new extrusion vector.
    Returns the new extrusion vector and a boolean value: ``True`` if the new
    OCS established by the new extrusion vector has a uniform scaled xy-plane,
    else ``False``.

    The new extrusion vector is perpendicular to plane defined by the
    transformed x- and y-axis.

    Args:
        extrusion: extrusion vector of the old OCS
        m: transformation matrix

    Returns:

    """
    ocs = OCS(extrusion)
    ocs_x_axis_in_wcs = ocs.to_wcs(X_AXIS)
    ocs_y_axis_in_wcs = ocs.to_wcs(Y_AXIS)
    x_axis, y_axis = m.transform_directions(
        (ocs_x_axis_in_wcs, ocs_y_axis_in_wcs)
    )

    # Check for uniform scaled xy-plane:
    is_uniform = math.isclose(
        x_axis.magnitude_square, y_axis.magnitude_square, abs_tol=1e-9
    )
    new_extrusion = x_axis.cross(y_axis).normalize()
    return new_extrusion, is_uniform


class OCSTransform:
    def __init__(self, extrusion: Vec3 = None, m: Matrix44 = None):
        self.m = m
        if extrusion is None:
            self.old_ocs = None
            self.scale_uniform = False
            self.new_ocs = None
        else:
            self.old_ocs = OCS(extrusion)
            new_extrusion, self.scale_uniform = transform_extrusion(
                extrusion, m
            )
            self.new_ocs = OCS(new_extrusion)

    @property
    def old_extrusion(self) -> Vec3:
        return self.old_ocs.uz

    @property
    def new_extrusion(self) -> Vec3:
        return self.new_ocs.uz

    @classmethod
    def from_ocs(cls, old: OCS, new: OCS, m: Matrix44) -> "OCSTransform":
        ocs = cls()
        ocs.m = m
        ocs.old_ocs = old
        ocs.new_ocs = new
        return ocs

    def transform_length(self, length: "Vertex", reflection=1.0) -> float:
        """Returns magnitude of `length` direction vector transformed from
        old OCS into new OCS including `reflection` correction applied.
        """
        return self.m.transform_direction(
            self.old_ocs.to_wcs(length)
        ).magnitude * sign(reflection)

    def transform_width(self, width: float) -> float:
        """Transform the width of a linear OCS entity from the old OCS
        into the new OCS. (LWPOLYLINE!)
        """
        if abs(width) > 1e-12:  # assume a uniform scaling!
            return max(
                self.transform_length((abs(width), 0, 0)),
                self.transform_length((0, abs(width), 0))
            )
        return 0.0

    transform_scale_factor = transform_length

    def transform_ocs_direction(self, direction: Vec3) -> Vec3:
        """Transform an OCS direction from the old OCS into the new OCS."""
        # OCS origin is ALWAYS the WCS origin!
        old_wcs_direction = self.old_ocs.to_wcs(direction)
        new_wcs_direction = self.m.transform_direction(old_wcs_direction)
        return self.new_ocs.from_wcs(new_wcs_direction)

    def transform_thickness(self, thickness: float) -> float:
        """Transform the thickness attribute of an OCS entity from the old OCS
        into the new OCS.

        Thickness is always applied in the z-axis direction of the OCS
        a.k.a. extrusion vector.

        """
        # Only the z-component of the thickness vector transformed into the
        # new OCS is relevant for the extrusion in the direction of the new
        # OCS z-axis.
        # Input and output thickness can be negative!
        new_ocs_thickness = self.transform_ocs_direction(Vec3(0, 0, thickness))
        return new_ocs_thickness.z

    def transform_vertex(self, vertex: "Vertex") -> Vec3:
        """Returns vertex transformed from old OCS into new OCS."""
        return self.new_ocs.from_wcs(
            self.m.transform(self.old_ocs.to_wcs(vertex))
        )

    def transform_2d_vertex(self, vertex: "Vertex", elevation: float) -> Vec2:
        """Returns 2D vertex transformed from old OCS into new OCS."""
        v = Vec3(vertex).replace(z=elevation)
        return self.new_ocs.from_wcs(
            self.m.transform(self.old_ocs.to_wcs(v))
        ).vec2

    def transform_direction(self, direction: "Vertex") -> Vec3:
        """Returns direction transformed from old OCS into new OCS."""
        return self.new_ocs.from_wcs(
            self.m.transform_direction(self.old_ocs.to_wcs(direction))
        )

    def transform_angle(self, angle: float) -> float:
        """Returns angle (in radians) from old OCS transformed into new OCS."""
        return self.transform_direction(Vec3.from_angle(angle)).angle

    def transform_deg_angle(self, angle: float) -> float:
        """Returns angle (in degrees) from old OCS transformed into new OCS."""
        return math.degrees(self.transform_angle(math.radians(angle)))

# Copyright (c) 2020-2021, <NAME>
# License: MIT License
from typing import TYPE_CHECKING, Tuple
import math
from ezdxf.math import Vec3, X_AXIS, Y_AXIS, Vec2, Matrix44, sign, OCS

if TYPE_CHECKING:
    from ezdxf.eztypes import DXFGraphic, Vertex

__all__ = [
    "TransformError",
    "NonUniformScalingError",
    "InsertTransformationError",
    "transform_extrusion",
    "transform_thickness_and_extrusion_without_ocs",
    "OCSTransform",
]


class TransformError(Exception):
    pass


class NonUniformScalingError(TransformError):
    pass


class InsertTransformationError(TransformError):
    pass


def transform_thickness_and_extrusion_without_ocs(
    entity: "DXFGraphic", m: Matrix44
) -> None:
    if entity.dxf.hasattr("thickness"):
        thickness = entity.dxf.thickness
        reflection = sign(thickness)
        thickness = m.transform_direction(entity.dxf.extrusion * thickness)
        entity.dxf.thickness = thickness.magnitude * reflection
        entity.dxf.extrusion = thickness.normalize()
    elif entity.dxf.hasattr("extrusion"):  # without thickness?
        extrusion = m.transform_direction(entity.dxf.extrusion)
        entity.dxf.extrusion = extrusion.normalize()


def transform_extrusion(extrusion: "Vertex", m: Matrix44) -> Tuple[Vec3, bool]:
    """Transforms the old `extrusion` vector into a new extrusion vector.
    Returns the new extrusion vector and a boolean value: ``True`` if the new
    OCS established by the new extrusion vector has a uniform scaled xy-plane,
    else ``False``.

    The new extrusion vector is perpendicular to plane defined by the
    transformed x- and y-axis.

    Args:
        extrusion: extrusion vector of the old OCS
        m: transformation matrix

    Returns:

    """
    ocs = OCS(extrusion)
    ocs_x_axis_in_wcs = ocs.to_wcs(X_AXIS)
    ocs_y_axis_in_wcs = ocs.to_wcs(Y_AXIS)
    x_axis, y_axis = m.transform_directions(
        (ocs_x_axis_in_wcs, ocs_y_axis_in_wcs)
    )

    # Check for uniform scaled xy-plane:
    is_uniform = math.isclose(
        x_axis.magnitude_square, y_axis.magnitude_square, abs_tol=1e-9
    )
    new_extrusion = x_axis.cross(y_axis).normalize()
    return new_extrusion, is_uniform


class OCSTransform:
    def __init__(self, extrusion: Vec3 = None, m: Matrix44 = None):
        self.m = m
        if extrusion is None:
            self.old_ocs = None
            self.scale_uniform = False
            self.new_ocs = None
        else:
            self.old_ocs = OCS(extrusion)
            new_extrusion, self.scale_uniform = transform_extrusion(
                extrusion, m
            )
            self.new_ocs = OCS(new_extrusion)

    @property
    def old_extrusion(self) -> Vec3:
        return self.old_ocs.uz

    @property
    def new_extrusion(self) -> Vec3:
        return self.new_ocs.uz

    @classmethod
    def from_ocs(cls, old: OCS, new: OCS, m: Matrix44) -> "OCSTransform":
        ocs = cls()
        ocs.m = m
        ocs.old_ocs = old
        ocs.new_ocs = new
        return ocs

    def transform_length(self, length: "Vertex", reflection=1.0) -> float:
        """Returns magnitude of `length` direction vector transformed from
        old OCS into new OCS including `reflection` correction applied.
        """
        return self.m.transform_direction(
            self.old_ocs.to_wcs(length)
        ).magnitude * sign(reflection)

    def transform_width(self, width: float) -> float:
        """Transform the width of a linear OCS entity from the old OCS
        into the new OCS. (LWPOLYLINE!)
        """
        if abs(width) > 1e-12:  # assume a uniform scaling!
            return max(
                self.transform_length((abs(width), 0, 0)),
                self.transform_length((0, abs(width), 0))
            )
        return 0.0

    transform_scale_factor = transform_length

    def transform_ocs_direction(self, direction: Vec3) -> Vec3:
        """Transform an OCS direction from the old OCS into the new OCS."""
        # OCS origin is ALWAYS the WCS origin!
        old_wcs_direction = self.old_ocs.to_wcs(direction)
        new_wcs_direction = self.m.transform_direction(old_wcs_direction)
        return self.new_ocs.from_wcs(new_wcs_direction)

    def transform_thickness(self, thickness: float) -> float:
        """Transform the thickness attribute of an OCS entity from the old OCS
        into the new OCS.

        Thickness is always applied in the z-axis direction of the OCS
        a.k.a. extrusion vector.

        """
        # Only the z-component of the thickness vector transformed into the
        # new OCS is relevant for the extrusion in the direction of the new
        # OCS z-axis.
        # Input and output thickness can be negative!
        new_ocs_thickness = self.transform_ocs_direction(Vec3(0, 0, thickness))
        return new_ocs_thickness.z

    def transform_vertex(self, vertex: "Vertex") -> Vec3:
        """Returns vertex transformed from old OCS into new OCS."""
        return self.new_ocs.from_wcs(
            self.m.transform(self.old_ocs.to_wcs(vertex))
        )

    def transform_2d_vertex(self, vertex: "Vertex", elevation: float) -> Vec2:
        """Returns 2D vertex transformed from old OCS into new OCS."""
        v = Vec3(vertex).replace(z=elevation)
        return self.new_ocs.from_wcs(
            self.m.transform(self.old_ocs.to_wcs(v))
        ).vec2

    def transform_direction(self, direction: "Vertex") -> Vec3:
        """Returns direction transformed from old OCS into new OCS."""
        return self.new_ocs.from_wcs(
            self.m.transform_direction(self.old_ocs.to_wcs(direction))
        )

    def transform_angle(self, angle: float) -> float:
        """Returns angle (in radians) from old OCS transformed into new OCS."""
        return self.transform_direction(Vec3.from_angle(angle)).angle

    def transform_deg_angle(self, angle: float) -> float:
        """Returns angle (in degrees) from old OCS transformed into new OCS."""
        return math.degrees(self.transform_angle(math.radians(angle)))

# Copyright (c) 2020-2021, <NAME> # License: MIT License # without thickness? Transforms the old `extrusion` vector into a new extrusion vector.     Returns the new extrusion vector and a boolean value: ``True`` if the new     OCS established by the new extrusion vector has a uniform scaled xy-plane,     else ``False``.      The new extrusion vector is perpendicular to plane defined by the     transformed x- and y-axis.      Args:         extrusion: extrusion vector of the old OCS         m: transformation matrix      Returns: # Check for uniform scaled xy-plane: Returns magnitude of `length` direction vector transformed from         old OCS into new OCS including `reflection` correction applied. Transform the width of a linear OCS entity from the old OCS         into the new OCS. (LWPOLYLINE!) # assume a uniform scaling! Transform an OCS direction from the old OCS into the new OCS. # OCS origin is ALWAYS the WCS origin! Transform the thickness attribute of an OCS entity from the old OCS         into the new OCS.          Thickness is always applied in the z-axis direction of the OCS         a.k.a. extrusion vector. # Only the z-component of the thickness vector transformed into the # new OCS is relevant for the extrusion in the direction of the new # OCS z-axis. # Input and output thickness can be negative! Returns vertex transformed from old OCS into new OCS. Returns 2D vertex transformed from old OCS into new OCS. Returns direction transformed from old OCS into new OCS. Returns angle (in radians) from old OCS transformed into new OCS. Returns angle (in degrees) from old OCS transformed into new OCS.