# Copyright (c) 2020, Manfred Moitzi
# License: MIT License
from typing import TYPE_CHECKING
import pytest
import random
import math
from ezdxf.entities import Circle, Arc, Ellipse, Insert
from ezdxf.math import Matrix44, Vec3, linspace, X_AXIS, Y_AXIS, Z_AXIS
import ezdxf

if TYPE_CHECKING:
    from ezdxf.eztypes import Drawing

UNIFORM_SCALING = [(2, 2, 2), (-1, 1, 1), (1, -1, 1), (1, 1, -1), (-2, -2, 2),
                   (2, -2, -2), (-2, 2, -2), (-3, -3, -3)]
NON_UNIFORM_SCALING = [(-1, 2, 3), (1, -2, 3), (1, 2, -3), (-3, -2, 1),
                       (3, -2, -1), (-3, 2, -1), (-3, -2, -1)]
SCALING_WITHOUT_REFLEXIONS = [(1, 1, 1), (2, 2, 2), (1, 2, 3)]


def synced_scaling(entity, chk, sx=1, sy=1, sz=1):
    entity = entity.copy()
    entity.scale(sx, sy, sz)
    chk = list(Matrix44.scale(sx, sy, sz).transform_vertices(chk))
    return entity, chk


def synced_rotation(entity, chk, axis, angle):
    entity = entity.copy()
    entity.rotate_axis(axis, angle)
    chk = list(Matrix44.axis_rotate(axis, angle).transform_vertices(chk))
    return entity, chk


def synced_translation(entity, chk, dx, dy, dz):
    entity = entity.copy()
    entity.translate(dx, dy, dz)
    chk = list(Matrix44.translate(dx, dy, dz).transform_vertices(chk))
    return entity, chk


def synced_transformation(entity, chk, m: Matrix44):
    entity = entity.copy()
    entity.transform(m)
    chk = list(m.transform_vertices(chk))
    return entity, chk


@pytest.mark.parametrize('sx, sy, sz', UNIFORM_SCALING)
def test_random_circle_transformation(sx, sy, sz):
    # testing only uniform scaling, for non uniform scaling
    # the circle has to be converted to an ellipse
    vertex_count = 8

    def build():
        circle = Circle()
        vertices = list(
            circle.vertices(linspace(0, 360, vertex_count, endpoint=False)))
        m = Matrix44.chain(
            Matrix44.axis_rotate(axis=Vec3.random(),
                                 angle=random.uniform(0, math.tau)),
            Matrix44.translate(dx=random.uniform(-2, 2),
                               dy=random.uniform(-2, 2),
                               dz=random.uniform(-2, 2)),
        )
        return synced_transformation(circle, vertices, m)

    def check(circle, vertices):
        # Vertex(angle=0) of old_ocs is not the vertex(angle=0) of the new OCS
        # because of the arbitrary Axis algorithm.

        # Checking center point:
        ocs = circle.ocs()
        wcs_circle_center = ocs.to_wcs(circle.dxf.center)
        vertices_center = vertices[0].lerp(vertices[int(vertex_count / 2)])
        assert wcs_circle_center.isclose(vertices_center, abs_tol=1e-9)

        # Check distance of vertices from circle center point:
        radius = circle.dxf.radius
        for vtx in vertices:
            assert math.isclose((vtx - wcs_circle_center).magnitude, radius,
                                abs_tol=1e-9)

        # Check for parallel plane orientation
        vertices_extrusion = (vertices[0] - vertices_center).cross(
            (vertices[1] - vertices_center))
        assert vertices_extrusion.is_parallel(circle.dxf.extrusion,
                                              abs_tol=1e-9)

    # test transformed circle against transformed WCS vertices of the circle
    for _ in range(10):
        circle0, vertices0 = build()
        check(circle0, vertices0)
        check(*synced_scaling(circle0, vertices0, sx, sy, sz))


@pytest.mark.parametrize('sx, sy, sz', UNIFORM_SCALING)
def test_random_arc_transformation(sx, sy, sz):
    # testing only uniform scaling, for non uniform scaling
    # the circle has to be converted to an ellipse
    vertex_count = 8

    def build():
        arc = Arc.new(dxfattribs={
            'start_angle': random.uniform(0, 360),
            'end_angle': random.uniform(0, 360),
        })
        vertices = list(arc.vertices(arc.angles(vertex_count)))
        m = Matrix44.chain(
            Matrix44.axis_rotate(axis=Vec3.random(),
                                 angle=random.uniform(0, math.tau)),
            Matrix44.translate(dx=random.uniform(-2, 2),
                               dy=random.uniform(-2, 2),
                               dz=random.uniform(-2, 2)),
        )
        return synced_transformation(arc, vertices, m)

    def check(arc, vertices):
        arc_vertices = arc.vertices(arc.angles(vertex_count))
        for vtx, chk in zip(arc_vertices, vertices):
            assert vtx.isclose(chk, abs_tol=1e-9)

    for _ in range(10):
        arc0, vertices0 = build()
        check(arc0, vertices0)
        check(*synced_scaling(arc0, vertices0, sx, sy, sz))



# Error conditions detected by github actions:
# ------------------------------------------------------------------------------
# 1. Ellipse transformation error condition failed on linux/CPython 3.7.10,
#    the random transformation is not known, the difference was much more than
#    just a precision issue:
#    Vec3(-0.3732124613203121, 3.932218038441924, -1.133607572247806) >
#       Vec3(-0.0036743564192640576, 5.229843031953656, 0.8619764018600419)
# ------------------------------------------------------------------------------
# @pytest.mark.parametrize('sx,sy,sz,start,end', [
#     (-1, 2, 3, 5.759586531581287, 0.5235987755982988)  # 1.
# ])
@pytest.mark.parametrize('sx, sy, sz', UNIFORM_SCALING + NON_UNIFORM_SCALING)
@pytest.mark.parametrize('start, end', [
    # closed ellipse fails at non uniform scaling test, because no start-
    # and end param adjustment is applied, so generated vertices do not
    # match test vertices.
    (0, math.pi),  # half ellipse as special case
    (math.pi / 6, math.pi / 6 * 11),  # start < end
    (math.pi / 6 * 11, math.pi / 6),  # start > end
])
def test_random_ellipse_transformations(sx, sy, sz, start, end):
    vertex_count = 8

    def build(angle, dx, dy, dz, axis):
        ellipse = Ellipse.new(dxfattribs={
            'start_param': start,
            'end_param': end,
        })
        vertices = list(ellipse.vertices(ellipse.params(vertex_count)))
        m = Matrix44.chain(
            Matrix44.axis_rotate(axis=axis, angle=angle),
            Matrix44.translate(dx=dx, dy=dy, dz=dz)
        )
        return synced_transformation(ellipse, vertices, m)

    def check(ellipse, vertices):
        ellipse_vertices = list(ellipse.vertices(ellipse.params(vertex_count)))
        # Ellipse vertices may appear in reverse order
        if not vertices[0].isclose(ellipse_vertices[0], abs_tol=1e-5):
            ellipse_vertices.reverse()

        for vtx, chk in zip(ellipse_vertices, vertices):
            assert vtx.isclose(chk, abs_tol=1e-5) is True, config

    for _ in range(10):
        angle = random.uniform(0, math.tau)
        dx = random.uniform(-2, 2)
        dy = random.uniform(-2, 2)
        dz = random.uniform(-2, 2)
        axis = Vec3.random()

        config = f"CONFIG sx={sx}, sy={sy}, sz={sz}; " \
                 f"start={start:.4f}, end={end:.4f}; " \
                 f"angle={angle}; dx={dx}, dy={dy}, dz={dz}; axis={str(axis)}"

        ellipse0, vertices0 = build(angle, dx, dy, dz, axis)
        check(ellipse0, vertices0)
        check(*synced_scaling(ellipse0, vertices0, sx, sy, sz))


@pytest.fixture(scope='module')
def doc1() -> 'Drawing':
    doc = ezdxf.new()
    blk = doc.blocks.new('AXIS')
    blk.add_line((0, 0, 0), X_AXIS, dxfattribs={'color': 1})
    blk.add_line((0, 0, 0), Y_AXIS, dxfattribs={'color': 3})
    blk.add_line((0, 0, 0), Z_AXIS, dxfattribs={'color': 5})
    return doc


@pytest.mark.parametrize('sx, sy, sz', UNIFORM_SCALING + NON_UNIFORM_SCALING)
def test_random_block_reference_transformation(sx, sy, sz, doc1: 'Drawing'):
    def insert():
        return Insert.new(dxfattribs={
            'name': 'AXIS',
            'insert': (0, 0, 0),
            'xscale': 1,
            'yscale': 1,
            'zscale': 1,
            'rotation': 0,
            'layer': 'insert',
        }, doc=doc1), [Vec3(0, 0, 0), X_AXIS, Y_AXIS, Z_AXIS]

    def check(lines, chk):
        origin, x, y, z = chk
        l1, l2, l3 = lines
        assert origin.isclose(l1.dxf.start)
        assert x.isclose(l1.dxf.end)
        assert origin.isclose(l2.dxf.start)
        assert y.isclose(l2.dxf.end)
        assert origin.isclose(l3.dxf.start)
        assert z.isclose(l3.dxf.end)

    entity0, vertices0 = insert()
    entity0, vertices0 = synced_scaling(entity0, vertices0, 1, 2, 3)

    m = Matrix44.chain(
        # Transformation order is important: scale - rotate - translate
        # Because scaling after rotation leads to a non orthogonal
        # coordinate system, which can not represented by the
        # INSERT entity.
        Matrix44.scale(sx, sy, sz),
        Matrix44.axis_rotate(axis=Vec3.random(),
                             angle=random.uniform(0, math.tau)),
        Matrix44.translate(dx=random.uniform(-2, 2), dy=random.uniform(-2, 2),
                           dz=random.uniform(-2, 2)),
    )
    entity, vertices = synced_transformation(entity0, vertices0, m)
    lines = list(entity.virtual_entities())
    check(lines, vertices)


@pytest.mark.parametrize('sx, sy, sz', [
    # Non uniform scaling will throw InsertTransformationError(),
    # because this multiple applied transformations cause non orthogonal
    # target coordinate systems, which can not represented by the INSERT entity.
    (1.1, 1.1, 1.1), (-1.1, -1.1, -1.1),
    (-1.1, 1.1, 1.1), (1.1, -1.1, 1.1), (1.1, 1.1, -1.1),
    (-1.1, -1.1, 1.1), (1.1, -1.1, -1.1), (-1.1, 1.1, -1.1),
])
def test_apply_transformation_multiple_times(sx, sy, sz, doc1: 'Drawing'):
    def insert():
        return Insert.new(dxfattribs={
            'name': 'AXIS',
            'insert': (0, 0, 0),
            'xscale': 1,
            'yscale': 1,
            'zscale': 1,
            'rotation': 0,
        }, doc=doc1), [(0, 0, 0), X_AXIS, Y_AXIS, Z_AXIS]

    entity, vertices = insert()
    m = Matrix44.chain(
        Matrix44.scale(sx, sy, sz),
        Matrix44.z_rotate(math.radians(10)),
        Matrix44.translate(1, 1, 1),
    )

    for i in range(5):
        entity, vertices = synced_transformation(entity, vertices, m)
        points = list(vertices)
        for num, line in enumerate(entity.virtual_entities()):
            assert points[0].isclose(line.dxf.start, abs_tol=1e-6)
            assert points[num + 1].isclose(line.dxf.end, abs_tol=1e-6)


if __name__ == '__main__':
    pytest.main([__file__])
