from math import pi

import numpy as np
import pytest
import openmc


@pytest.mark.parametrize("val_left,val_right", [(0, 0), (-1., -1.), (2.0, 2)])
def test_raises_error_when_flat(val_left, val_right):
    """Checks that an error is raised when a mesh is flat"""
    mesh = openmc.RegularMesh()

    # Same X
    with pytest.raises(ValueError):
        mesh.lower_left = [val_left, -25, -25]
        mesh.upper_right = [val_right, 25, 25]

    with pytest.raises(ValueError):
        mesh.upper_right = [val_right, 25, 25]
        mesh.lower_left = [val_left, -25, -25]

    # Same Y
    with pytest.raises(ValueError):
        mesh.lower_left = [-25, val_left, -25]
        mesh.upper_right = [25, val_right, 25]

    with pytest.raises(ValueError):
        mesh.upper_right = [25, val_right, 25]
        mesh.lower_left = [-25, val_left, -25]

    # Same Z
    with pytest.raises(ValueError):
        mesh.lower_left = [-25, -25, val_left]
        mesh.upper_right = [25, 25, val_right]

    with pytest.raises(ValueError):
        mesh.upper_right = [25, 25, val_right]
        mesh.lower_left = [-25, -25, val_left]


def test_regular_mesh_bounding_box():
    mesh = openmc.RegularMesh()
    mesh.lower_left = [-2, -3, -5]
    mesh.upper_right = [2, 3, 5]
    bb = mesh.bounding_box
    assert isinstance(bb, openmc.BoundingBox)
    np.testing.assert_array_equal(bb.lower_left, (-2, -3 ,-5))
    np.testing.assert_array_equal(bb.upper_right, (2, 3, 5))


def test_rectilinear_mesh_bounding_box():
    mesh = openmc.RectilinearMesh()
    mesh.x_grid = [0., 1., 5., 10.]
    mesh.y_grid = [-10., -5., 0.]
    mesh.z_grid = [-100., 0., 100.]
    bb = mesh.bounding_box
    assert isinstance(bb, openmc.BoundingBox)
    np.testing.assert_array_equal(bb.lower_left, (0., -10. ,-100.))
    np.testing.assert_array_equal(bb.upper_right, (10., 0., 100.))


def test_cylindrical_mesh_bounding_box():
    # test with mesh at origin (0, 0, 0)
    mesh = openmc.CylindricalMesh(
        r_grid=[0.1, 0.2, 0.5, 1.],
        z_grid=[0.1, 0.2, 0.4, 0.6, 1.],
        origin=(0, 0, 0)
    )
    np.testing.assert_array_equal(mesh.upper_right, (1, 1, 1))
    np.testing.assert_array_equal(mesh.lower_left, (-1, -1, 0.1))
    bb = mesh.bounding_box
    assert isinstance(bb, openmc.BoundingBox)
    np.testing.assert_array_equal(bb.lower_left, (-1, -1, 0.1))
    np.testing.assert_array_equal(bb.upper_right, (1, 1, 1))

    # test with mesh at origin (3, 5, 7)
    mesh.origin = (3, 5, 7)
    np.testing.assert_array_equal(mesh.upper_right, (4, 6, 8))
    np.testing.assert_array_equal(mesh.lower_left, (2, 4, 7.1))
    bb = mesh.bounding_box
    assert isinstance(bb, openmc.BoundingBox)
    np.testing.assert_array_equal(bb.lower_left, (2, 4, 7.1))
    np.testing.assert_array_equal(bb.upper_right, (4, 6, 8))

    # changing z grid to contain negative numbers
    mesh.z_grid = [-10, 0, 10]
    np.testing.assert_array_equal(mesh.lower_left, (2, 4, -3))
    np.testing.assert_array_equal(mesh.upper_right, (4, 6, 17))


def test_spherical_mesh_bounding_box():
    # test with mesh at origin (0, 0, 0)
    mesh = openmc.SphericalMesh([0.1, 0.2, 0.5, 1.], origin=(0., 0., 0.))
    np.testing.assert_array_equal(mesh.upper_right, (1, 1, 1))
    np.testing.assert_array_equal(mesh.lower_left, (-1, -1, -1))
    bb = mesh.bounding_box
    assert isinstance(bb, openmc.BoundingBox)
    np.testing.assert_array_equal(bb.lower_left, (-1, -1, -1))
    np.testing.assert_array_equal(bb.upper_right, (1, 1, 1))

    # test with mesh at origin (3, 5, 7)
    mesh.origin = (3, 5, 7)
    np.testing.assert_array_equal(mesh.upper_right, (4, 6, 8))
    np.testing.assert_array_equal(mesh.lower_left, (2, 4, 6))
    bb = mesh.bounding_box
    assert isinstance(bb, openmc.BoundingBox)
    np.testing.assert_array_equal(bb.lower_left, (2, 4, 6))
    np.testing.assert_array_equal(bb.upper_right, (4, 6, 8))


def test_SphericalMesh_initiation():
    # test defaults
    mesh = openmc.SphericalMesh(r_grid=(0, 10))
    assert (mesh.origin == np.array([0, 0, 0])).all()
    assert (mesh.r_grid == np.array([0, 10])).all()
    assert (mesh.theta_grid == np.array([0, pi])).all()
    assert (mesh.phi_grid == np.array([0, 2*pi])).all()

    # test setting on creation
    mesh = openmc.SphericalMesh(
        origin=(1, 2, 3),
        r_grid=(0, 2),
        theta_grid=(1, 3),
        phi_grid=(2, 4)
    )
    assert (mesh.origin == np.array([1, 2, 3])).all()
    assert (mesh.r_grid == np.array([0., 2.])).all()
    assert (mesh.theta_grid == np.array([1, 3])).all()
    assert (mesh.phi_grid == np.array([2, 4])).all()

    # test attribute changing
    mesh.r_grid = (0, 11)
    assert (mesh.r_grid == np.array([0., 11.])).all()

    # waffles and pancakes are unfortunately not valid radii
    with pytest.raises(TypeError):
        openmc.SphericalMesh(('🧇', '🥞'))


def test_CylindricalMesh_initiation():
    # test defaults
    mesh = openmc.CylindricalMesh(r_grid=(0, 10), z_grid=(0, 10))
    assert (mesh.origin == np.array([0, 0, 0])).all()
    assert (mesh.r_grid == np.array([0, 10])).all()
    assert (mesh.phi_grid == np.array([0, 2*pi])).all()
    assert (mesh.z_grid == np.array([0, 10])).all()

    # test setting on creation
    mesh = openmc.CylindricalMesh(
        origin=(1, 2, 3),
        r_grid=(0, 2),
        z_grid=(1, 3),
        phi_grid=(2, 4)
    )
    assert (mesh.origin == np.array([1, 2, 3])).all()
    assert (mesh.r_grid == np.array([0., 2.])).all()
    assert (mesh.z_grid == np.array([1, 3])).all()
    assert (mesh.phi_grid == np.array([2, 4])).all()

    # test attribute changing
    mesh.r_grid = (0., 10.)
    assert (mesh.r_grid == np.array([0, 10.])).all()
    mesh.z_grid = (0., 4.)
    assert (mesh.z_grid == np.array([0, 4.])).all()

    # waffles and pancakes are unfortunately not valid radii
    with pytest.raises(TypeError):
        openmc.SphericalMesh(('🧇', '🥞'))


def test_centroids():
    # regular mesh
    mesh = openmc.RegularMesh()
    mesh.lower_left = (1., 2., 3.)
    mesh.upper_right = (11., 12., 13.)
    mesh.dimension = (1, 1, 1)
    np.testing.assert_array_almost_equal(mesh.centroids[0, 0, 0], [6., 7., 8.])

    # rectilinear mesh
    mesh = openmc.RectilinearMesh()
    mesh.x_grid = [1., 11.]
    mesh.y_grid = [2., 12.]
    mesh.z_grid = [3., 13.]
    np.testing.assert_array_almost_equal(mesh.centroids[0, 0, 0], [6., 7., 8.])

    # cylindrical mesh
    mesh = openmc.CylindricalMesh(r_grid=(0, 10), z_grid=(0, 10), phi_grid=(0, np.pi))
    np.testing.assert_array_almost_equal(mesh.centroids[0, 0, 0], [0.0, 5.0, 5.0])
    # ensure that setting an origin is handled correctly
    mesh.origin = (5.0, 0, -10)
    np.testing.assert_array_almost_equal(mesh.centroids[0, 0, 0], [5.0, 5.0, -5.0])

    # spherical mesh, single element xyz-positive octant
    mesh = openmc.SphericalMesh(r_grid=[0, 10], theta_grid=[0, 0.5*np.pi], phi_grid=[0, np.pi])
    x = 5.*np.cos(0.5*np.pi)*np.sin(0.25*np.pi)
    y = 5.*np.sin(0.5*np.pi)*np.sin(0.25*np.pi)
    z = 5.*np.sin(0.25*np.pi)
    np.testing.assert_array_almost_equal(mesh.centroids[0, 0, 0], [x, y, z])

    mesh.origin = (-5.0, -5.0, 5.0)
    np.testing.assert_array_almost_equal(mesh.centroids[0, 0, 0], [x-5.0, y-5.0, z+5.0])


@pytest.mark.parametrize('mesh_type', ('regular', 'rectilinear', 'cylindrical', 'spherical'))
def test_mesh_vertices(mesh_type):

    ijk = (2, 3, 2)

    # create a new mesh object
    if mesh_type == 'regular':
        mesh = openmc.RegularMesh()
        ll = np.asarray([0.]*3)
        width = np.asarray([0.5]*3)
        mesh.lower_left = ll
        mesh.width = width
        mesh.dimension = (5, 7, 9)

        # spot check that an element has the correct vertex coordinates asociated with it
        # (using zero-indexing here)
        exp_i_j_k = ll + np.asarray(ijk, dtype=float) * width
        np.testing.assert_equal(mesh.vertices[ijk], exp_i_j_k)

        # shift the mesh using the llc
        shift  = np.asarray((3.0, 6.0, 10.0))
        mesh.lower_left += shift
        np.testing.assert_equal(mesh.vertices[ijk], exp_i_j_k+shift)
    elif mesh_type == 'rectilinear':
        mesh = openmc.RectilinearMesh()
        w = np.asarray([0.5] * 3)
        ll = np.asarray([0.]*3)
        dims = (5, 7, 9)
        mesh.x_grid = np.linspace(ll[0], w[0]*dims[0], dims[0])
        mesh.y_grid = np.linspace(ll[1], w[1]*dims[1], dims[1])
        mesh.z_grid = np.linspace(ll[2], w[2]*dims[2], dims[2])
        exp_vert = np.asarray((mesh.x_grid[2], mesh.y_grid[3], mesh.z_grid[2]))
        np.testing.assert_equal(mesh.vertices[ijk], exp_vert)
    elif mesh_type == 'cylindrical':
        r_grid = np.linspace(0, 5, 10)
        z_grid = np.linspace(-10, 10, 20)
        phi_grid = np.linspace(0, 2*np.pi, 8)
        mesh = openmc.CylindricalMesh(r_grid=r_grid, z_grid=z_grid, phi_grid=phi_grid)
        exp_vert = np.asarray((mesh.r_grid[2], mesh.phi_grid[3], mesh.z_grid[2]))
        np.testing.assert_equal(mesh.vertices_cylindrical[ijk], exp_vert)
    elif mesh_type == 'spherical':
        r_grid = np.linspace(0, 13, 14)
        theta_grid = np.linspace(0, np.pi, 11)
        phi_grid = np.linspace(0, 2*np.pi, 7)
        mesh = openmc.SphericalMesh(r_grid=r_grid, theta_grid=theta_grid, phi_grid=phi_grid)
        exp_vert = np.asarray((mesh.r_grid[2], mesh.theta_grid[3], mesh.phi_grid[2]))
        np.testing.assert_equal(mesh.vertices_spherical[ijk], exp_vert)


def test_CylindricalMesh_get_indices_at_coords():
    # default origin (0, 0, 0) and default phi grid (0, 2*pi)
    mesh = openmc.CylindricalMesh(r_grid=(0, 5, 10), z_grid=(0, 5, 10))
    assert mesh.get_indices_at_coords([1, 0, 1]) == (0, 0, 0)
    assert mesh.get_indices_at_coords([6, 0, 1]) == (1, 0, 0)
    assert mesh.get_indices_at_coords([9, 0, 1]) == (1, 0, 0)
    assert mesh.get_indices_at_coords([0, 6, 0]) == (1, 0, 0)
    assert mesh.get_indices_at_coords([0, 9, 6]) == (1, 0, 1)
    assert mesh.get_indices_at_coords([-2, -2, 9]) == (0, 0, 1)

    with pytest.raises(ValueError):
        assert mesh.get_indices_at_coords([8, 8, 1])  # resulting r value to large
    with pytest.raises(ValueError):
        assert mesh.get_indices_at_coords([-8, -8, 1])  # resulting r value to large
    with pytest.raises(ValueError):
        assert mesh.get_indices_at_coords([1, 0, -1])  # z value below range
    with pytest.raises(ValueError):
        assert mesh.get_indices_at_coords([1, 0, 11])  # z value above range

    assert mesh.get_indices_at_coords([1, 1, 1]) == (0, 0, 0)

    # negative range on z grid
    mesh = openmc.CylindricalMesh(
        r_grid=(0, 5, 10),
        phi_grid=(0, 0.5 * pi, pi, 1.5 * pi, 1.9 * pi),
        z_grid=(-5, 0, 5, 10),
    )
    assert mesh.get_indices_at_coords([1, 1, 1]) == (0, 0, 1)  # first angle quadrant
    assert mesh.get_indices_at_coords([2, 2, 6]) == (0, 0, 2)  # first angle quadrant
    assert mesh.get_indices_at_coords([-2, 0.1, -1]) == (0, 1, 0)  # second angle quadrant
    assert mesh.get_indices_at_coords([-2, -0.1, -1]) == (0, 2, 0)  # third angle quadrant
    assert mesh.get_indices_at_coords([2, -0.9, -1]) == (0, 3, 0)  # forth angle quadrant

    with pytest.raises(ValueError):
        assert mesh.get_indices_at_coords([2, -0.1, 1])  # outside of phi range

    # origin of mesh not default
    mesh = openmc.CylindricalMesh(
        r_grid=(0, 5, 10),
        phi_grid=(0, 0.5 * pi, pi, 1.5 * pi, 1.9 * pi),
        z_grid=(-5, 0, 5, 10),
        origin=(100, 200, 300),
    )
    assert mesh.get_indices_at_coords([101, 201, 301]) == (0, 0, 1)  # first angle quadrant
    assert mesh.get_indices_at_coords([102, 202, 306]) == (0, 0, 2)  # first angle quadrant
    assert mesh.get_indices_at_coords([98, 200.1, 299]) == (0, 1, 0)  # second angle quadrant
    assert mesh.get_indices_at_coords([98, 199.9, 299]) == (0, 2, 0)  # third angle quadrant
    assert mesh.get_indices_at_coords([102, 199.1, 299]) == (0, 3, 0)  # forth angle quadrant
