import unittest
from math import pi

import numpy as np
import pytest

from shapely import affinity
from shapely.geometry import Point
from shapely.wkt import loads as load_wkt


class AffineTestCase(unittest.TestCase):
    def test_affine_params(self):
        g = load_wkt("LINESTRING(2.4 4.1, 2.4 3, 3 3)")

        with pytest.raises(TypeError):
            affinity.affine_transform(g, None)

        with pytest.raises(ValueError):
            affinity.affine_transform(g, [1, 2, 3, 4, 5, 6, 7, 8, 9])

        with pytest.raises(AttributeError):
            affinity.affine_transform(None, [1, 2, 3, 4, 5, 6])

    def test_affine_geom_types(self):

        # identity matrices, which should result with no transformation
        matrix2d = (1, 0, 0, 1, 0, 0)
        matrix3d = (1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0)

        # empty in, empty out
        empty2d = load_wkt("MULTIPOLYGON EMPTY")
        assert affinity.affine_transform(empty2d, matrix2d).is_empty

        def test_geom(g2, g3=None):
            assert not g2.has_z
            a2 = affinity.affine_transform(g2, matrix2d)
            assert not a2.has_z
            assert g2.equals(a2)
            if g3 is not None:
                assert g3.has_z
                a3 = affinity.affine_transform(g3, matrix3d)
                assert a3.has_z
                assert g3.equals(a3)
            return

        pt2d = load_wkt("POINT(12.3 45.6)")
        pt3d = load_wkt("POINT(12.3 45.6 7.89)")
        test_geom(pt2d, pt3d)
        ls2d = load_wkt("LINESTRING(0.9 3.4, 0.7 2, 2.5 2.7)")
        ls3d = load_wkt("LINESTRING(0.9 3.4 3.3, 0.7 2 2.3, 2.5 2.7 5.5)")
        test_geom(ls2d, ls3d)
        lr2d = load_wkt("LINEARRING(0.9 3.4, 0.7 2, 2.5 2.7, 0.9 3.4)")
        lr3d = load_wkt("LINEARRING(0.9 3.4 3.3, 0.7 2 2.3, 2.5 2.7 5.5, 0.9 3.4 3.3)")
        test_geom(lr2d, lr3d)
        test_geom(
            load_wkt(
                "POLYGON((0.9 2.3, 0.5 1.1, 2.4 0.8, 0.9 2.3), "
                "(1.1 1.7, 0.9 1.3, 1.4 1.2, 1.1 1.7), "
                "(1.6 1.3, 1.7 1, 1.9 1.1, 1.6 1.3))"
            )
        )
        test_geom(
            load_wkt("MULTIPOINT ((-300 300), (700 300), (-800 -1100), (200 -300))")
        )
        test_geom(
            load_wkt(
                "MULTILINESTRING((0 0, -0.7 -0.7, 0.6 -1), "
                "(-0.5 0.5, 0.7 0.6, 0 -0.6))"
            )
        )
        test_geom(
            load_wkt(
                "MULTIPOLYGON(((900 4300, -1100 -400, 900 -800, 900 4300)), "
                "((1200 4300, 2300 4400, 1900 1000, 1200 4300)))"
            )
        )
        test_geom(
            load_wkt(
                "GEOMETRYCOLLECTION(POINT(20 70),"
                " POLYGON((60 70, 13 35, 60 -30, 60 70)),"
                " LINESTRING(60 70, 50 100, 80 100))"
            )
        )

    def test_affine_2d(self):
        g = load_wkt("LINESTRING(2.4 4.1, 2.4 3, 3 3)")
        # custom scale and translate
        expected2d = load_wkt("LINESTRING(-0.2 14.35, -0.2 11.6, 1 11.6)")
        matrix2d = (2, 0, 0, 2.5, -5, 4.1)
        a2 = affinity.affine_transform(g, matrix2d)
        assert a2.equals_exact(expected2d, 1e-6)
        assert not a2.has_z
        # Make sure a 3D matrix does not make a 3D shape from a 2D input
        matrix3d = (2, 0, 0, 0, 2.5, 0, 0, 0, 10, -5, 4.1, 100)
        a3 = affinity.affine_transform(g, matrix3d)
        assert a3.equals_exact(expected2d, 1e-6)
        assert not a3.has_z

    def test_affine_3d(self):
        g2 = load_wkt("LINESTRING(2.4 4.1, 2.4 3, 3 3)")
        g3 = load_wkt("LINESTRING(2.4 4.1 100.2, 2.4 3 132.8, 3 3 128.6)")
        # custom scale and translate
        matrix2d = (2, 0, 0, 2.5, -5, 4.1)
        matrix3d = (2, 0, 0, 0, 2.5, 0, 0, 0, 0.3048, -5, 4.1, 100)
        # Combinations of 2D and 3D geometries and matrices
        a22 = affinity.affine_transform(g2, matrix2d)
        a23 = affinity.affine_transform(g2, matrix3d)
        a32 = affinity.affine_transform(g3, matrix2d)
        a33 = affinity.affine_transform(g3, matrix3d)
        # Check dimensions
        assert not a22.has_z
        assert not a23.has_z
        assert a32.has_z
        assert a33.has_z

        # 2D equality checks
        expected2d = load_wkt("LINESTRING(-0.2 14.35, -0.2 11.6, 1 11.6)")
        expected3d = load_wkt(
            "LINESTRING(-0.2 14.35 130.54096, " "-0.2 11.6 140.47744, 1 11.6 139.19728)"
        )
        expected32 = load_wkt(
            "LINESTRING(-0.2 14.35 100.2, " "-0.2 11.6 132.8, 1 11.6 128.6)"
        )
        assert a22.equals_exact(expected2d, 1e-6)
        assert a23.equals_exact(expected2d, 1e-6)
        # Do explicit 3D check of coordinate values
        for a, e in zip(a32.coords, expected32.coords):
            for ap, ep in zip(a, e):
                self.assertAlmostEqual(ap, ep)
        for a, e in zip(a33.coords, expected3d.coords):
            for ap, ep in zip(a, e):
                self.assertAlmostEqual(ap, ep)


class TransformOpsTestCase(unittest.TestCase):
    def test_rotate(self):
        ls = load_wkt("LINESTRING(240 400, 240 300, 300 300)")
        # counter-clockwise degrees
        rls = affinity.rotate(ls, 90)
        els = load_wkt("LINESTRING(220 320, 320 320, 320 380)")
        assert rls.equals(els)
        # retest with named parameters for the same result
        rls = affinity.rotate(geom=ls, angle=90, origin="center")
        assert rls.equals(els)
        # clockwise radians
        rls = affinity.rotate(ls, -pi / 2, use_radians=True)
        els = load_wkt("LINESTRING(320 380, 220 380, 220 320)")
        assert rls.equals(els)
        ## other `origin` parameters
        # around the centroid
        rls = affinity.rotate(ls, 90, origin="centroid")
        els = load_wkt("LINESTRING(182.5 320, 282.5 320, 282.5 380)")
        assert rls.equals(els)
        # around the second coordinate tuple
        rls = affinity.rotate(ls, 90, origin=ls.coords[1])
        els = load_wkt("LINESTRING(140 300, 240 300, 240 360)")
        assert rls.equals(els)
        # around the absolute Point of origin
        rls = affinity.rotate(ls, 90, origin=Point(0, 0))
        els = load_wkt("LINESTRING(-400 240, -300 240, -300 300)")
        assert rls.equals(els)

    def test_rotate_empty(self):
        rls = affinity.rotate(load_wkt("LINESTRING EMPTY"), 90)
        els = load_wkt("LINESTRING EMPTY")
        assert rls.equals(els)

    def test_rotate_angle_array(self):
        ls = load_wkt("LINESTRING(240 400, 240 300, 300 300)")
        els = load_wkt("LINESTRING(220 320, 320 320, 320 380)")
        # check with degrees
        theta = np.array(90.0)
        rls = affinity.rotate(ls, theta)
        assert theta.item() == 90.0
        assert rls.equals(els)
        # check with radians
        theta = np.array(pi / 2)
        rls = affinity.rotate(ls, theta, use_radians=True)
        assert theta.item() == pi / 2
        assert rls.equals(els)

    def test_scale(self):
        ls = load_wkt("LINESTRING(240 400 10, 240 300 30, 300 300 20)")
        # test defaults of 1.0
        sls = affinity.scale(ls)
        assert sls.equals(ls)
        # different scaling in different dimensions
        sls = affinity.scale(ls, 2, 3, 0.5)
        els = load_wkt("LINESTRING(210 500 5, 210 200 15, 330 200 10)")
        assert sls.equals(els)
        # Do explicit 3D check of coordinate values
        for a, b in zip(sls.coords, els.coords):
            for ap, bp in zip(a, b):
                self.assertEqual(ap, bp)
        # retest with named parameters for the same result
        sls = affinity.scale(geom=ls, xfact=2, yfact=3, zfact=0.5, origin="center")
        assert sls.equals(els)
        ## other `origin` parameters
        # around the centroid
        sls = affinity.scale(ls, 2, 3, 0.5, origin="centroid")
        els = load_wkt("LINESTRING(228.75 537.5, 228.75 237.5, 348.75 237.5)")
        assert sls.equals(els)
        # around the second coordinate tuple
        sls = affinity.scale(ls, 2, 3, 0.5, origin=ls.coords[1])
        els = load_wkt("LINESTRING(240 600, 240 300, 360 300)")
        assert sls.equals(els)
        # around some other 3D Point of origin
        sls = affinity.scale(ls, 2, 3, 0.5, origin=Point(100, 200, 1000))
        els = load_wkt("LINESTRING(380 800 505, 380 500 515, 500 500 510)")
        assert sls.equals(els)
        # Do explicit 3D check of coordinate values
        for a, b in zip(sls.coords, els.coords):
            for ap, bp in zip(a, b):
                assert ap == bp

    def test_scale_empty(self):
        sls = affinity.scale(load_wkt("LINESTRING EMPTY"))
        els = load_wkt("LINESTRING EMPTY")
        assert sls.equals(els)

    def test_skew(self):
        ls = load_wkt("LINESTRING(240 400 10, 240 300 30, 300 300 20)")
        # test default shear angles of 0.0
        sls = affinity.skew(ls)
        assert sls.equals(ls)
        # different shearing in x- and y-directions
        sls = affinity.skew(ls, 15, -30)
        els = load_wkt(
            "LINESTRING (253.39745962155615 417.3205080756888, "
            "226.60254037844385 317.3205080756888, "
            "286.60254037844385 282.67949192431126)"
        )
        assert sls.equals_exact(els, 1e-6)
        # retest with radians for the same result
        sls = affinity.skew(ls, pi / 12, -pi / 6, use_radians=True)
        assert sls.equals_exact(els, 1e-6)
        # retest with named parameters for the same result
        sls = affinity.skew(geom=ls, xs=15, ys=-30, origin="center", use_radians=False)
        assert sls.equals_exact(els, 1e-6)
        ## other `origin` parameters
        # around the centroid
        sls = affinity.skew(ls, 15, -30, origin="centroid")
        els = load_wkt(
            "LINESTRING(258.42150697963973 406.49519052838332, "
            "231.6265877365273980 306.4951905283833185, "
            "291.6265877365274264 271.8541743770057337)"
        )
        assert sls.equals_exact(els, 1e-6)
        # around the second coordinate tuple
        sls = affinity.skew(ls, 15, -30, origin=ls.coords[1])
        els = load_wkt(
            "LINESTRING(266.7949192431123038 400, 240 300, " "300 265.3589838486224153)"
        )
        assert sls.equals_exact(els, 1e-6)
        # around the absolute Point of origin
        sls = affinity.skew(ls, 15, -30, origin=Point(0, 0))
        els = load_wkt(
            "LINESTRING(347.179676972449101 261.435935394489832, "
            "320.3847577293367976 161.4359353944898317, "
            "380.3847577293367976 126.7949192431122754)"
        )
        assert sls.equals_exact(els, 1e-6)

    def test_skew_empty(self):
        sls = affinity.skew(load_wkt("LINESTRING EMPTY"))
        els = load_wkt("LINESTRING EMPTY")
        assert sls.equals(els)

    def test_skew_xs_ys_array(self):
        ls = load_wkt("LINESTRING(240 400 10, 240 300 30, 300 300 20)")
        els = load_wkt(
            "LINESTRING (253.39745962155615 417.3205080756888, "
            "226.60254037844385 317.3205080756888, "
            "286.60254037844385 282.67949192431126)"
        )
        # check with degrees
        xs_ys = np.array([15.0, -30.0])
        sls = affinity.skew(ls, xs_ys[0, ...], xs_ys[1, ...])
        assert xs_ys[0] == 15.0
        assert xs_ys[1] == -30.0
        assert sls.equals_exact(els, 1e-6)
        # check with radians
        xs_ys = np.array([pi / 12, -pi / 6])
        sls = affinity.skew(ls, xs_ys[0, ...], xs_ys[1, ...], use_radians=True)
        assert xs_ys[0] == pi / 12
        assert xs_ys[1] == -pi / 6
        assert sls.equals_exact(els, 1e-6)

    def test_translate(self):
        ls = load_wkt("LINESTRING(240 400 10, 240 300 30, 300 300 20)")
        # test default offset of 0.0
        tls = affinity.translate(ls)
        assert tls.equals(ls)
        # test all offsets
        tls = affinity.translate(ls, 100, 400, -10)
        els = load_wkt("LINESTRING(340 800 0, 340 700 20, 400 700 10)")
        assert tls.equals(els)
        # Do explicit 3D check of coordinate values
        for a, b in zip(tls.coords, els.coords):
            for ap, bp in zip(a, b):
                assert ap == bp
        # retest with named parameters for the same result
        tls = affinity.translate(geom=ls, xoff=100, yoff=400, zoff=-10)
        assert tls.equals(els)

    def test_translate_empty(self):
        tls = affinity.translate(load_wkt("LINESTRING EMPTY"))
        els = load_wkt("LINESTRING EMPTY")
        self.assertTrue(tls.equals(els))
        assert tls.equals(els)
