import lxml.etree as ET
from pathlib import Path

import numpy as np
import openmc
import pytest


def test_volume(run_in_tmpdir, uo2):
    """Test adding volume information from a volume calculation."""
    # Create model with nested spheres
    model = openmc.model.Model()
    model.materials.append(uo2)
    inner = openmc.Sphere(r=1.)
    outer = openmc.Sphere(r=2., boundary_type='vacuum')
    c1 = openmc.Cell(fill=uo2, region=-inner)
    c2 = openmc.Cell(region=+inner & -outer)
    u = openmc.Universe(cells=[c1, c2])
    model.geometry.root_universe = u
    model.settings.particles = 100
    model.settings.batches = 10
    model.settings.run_mode = 'fixed source'
    model.settings.source = openmc.IndependentSource(space=openmc.stats.Point())

    ll, ur = model.geometry.bounding_box
    assert ll == pytest.approx((-outer.r, -outer.r, -outer.r))
    assert ur == pytest.approx((outer.r, outer.r, outer.r))
    model.settings.volume_calculations

    for domain in (c1, uo2, u):
        # Run stochastic volume calculation
        volume_calc = openmc.VolumeCalculation(
            domains=[domain], samples=1000, lower_left=ll, upper_right=ur)
        model.settings.volume_calculations = [volume_calc]
        model.export_to_xml()
        openmc.calculate_volumes()

        # Load results and add volume information
        volume_calc.load_results('volume_1.h5')
        model.geometry.add_volume_information(volume_calc)

        # get_nuclide_densities relies on volume information
        nucs = set(domain.get_nuclide_densities())
        assert not nucs ^ {'U235', 'O16'}


def test_export_xml(run_in_tmpdir, uo2):
    s1 = openmc.Sphere(r=1.)
    s2 = openmc.Sphere(r=2., boundary_type='reflective')
    c1 = openmc.Cell(fill=uo2, region=-s1)
    c2 = openmc.Cell(fill=uo2, region=+s1 & -s2)
    geom = openmc.Geometry([c1, c2])
    geom.export_to_xml()

    doc = ET.parse('geometry.xml')
    root = doc.getroot()
    assert root.tag == 'geometry'
    cells = root.findall('cell')
    assert [int(c.get('id')) for c in cells] == [c1.id, c2.id]
    surfs = root.findall('surface')
    assert [int(s.get('id')) for s in surfs] == [s1.id, s2.id]


def test_find(uo2):
    xp = openmc.XPlane()
    c1 = openmc.Cell(fill=uo2, region=+xp)
    c2 = openmc.Cell(region=-xp)
    u1 = openmc.Universe(cells=(c1, c2))

    cyl = openmc.ZCylinder()
    c3 = openmc.Cell(fill=u1, region=-cyl)
    c4 = openmc.Cell(region=+cyl)
    geom = openmc.Geometry((c3, c4))

    seq = geom.find((0.5, 0., 0.))
    assert seq[-1] == c1
    seq = geom.find((-0.5, 0., 0.))
    assert seq[-1] == c2
    seq = geom.find((-1.5, 0., 0.))
    assert seq[-1] == c4


def test_get_all_cells():
    cells = [openmc.Cell() for i in range(5)]
    cells2 = [openmc.Cell() for i in range(3)]
    cells[0].fill = openmc.Universe(cells=cells2)
    geom = openmc.Geometry(cells)

    all_cells = set(geom.get_all_cells().values())
    assert not all_cells ^ set(cells + cells2)


def test_get_all_materials():
    m1 = openmc.Material()
    m2 = openmc.Material()
    c1 = openmc.Cell(fill=m1)
    u1 = openmc.Universe(cells=[c1])

    s = openmc.Sphere()
    c2 = openmc.Cell(fill=u1, region=-s)
    c3 = openmc.Cell(fill=m2, region=+s)
    geom = openmc.Geometry([c2, c3])

    all_mats = set(geom.get_all_materials().values())
    assert not all_mats ^ {m1, m2}


def test_get_all_material_cells():
    m1 = openmc.Material()
    m2 = openmc.Material()
    c1 = openmc.Cell(fill=m1)
    u1 = openmc.Universe(cells=[c1])

    s = openmc.Sphere()
    c2 = openmc.Cell(fill=u1, region=-s)
    c3 = openmc.Cell(fill=m2, region=+s)
    geom = openmc.Geometry([c2, c3])

    all_cells = set(geom.get_all_material_cells().values())
    assert not all_cells ^ {c1, c3}


def test_get_all_material_universes():
    m1 = openmc.Material()
    m2 = openmc.Material()
    c1 = openmc.Cell(fill=m1)
    u1 = openmc.Universe(cells=[c1])

    s = openmc.Sphere()
    c2 = openmc.Cell(fill=u1, region=-s)
    c3 = openmc.Cell(fill=m2, region=+s)
    geom = openmc.Geometry([c2, c3])

    all_univs = set(geom.get_all_material_universes().values())
    assert not all_univs ^ {u1, geom.root_universe}


def test_get_all_lattices(cell_with_lattice):
    cells, mats, univ, lattice = cell_with_lattice
    geom = openmc.Geometry([cells[-1]])

    lats = list(geom.get_all_lattices().values())
    assert lats == [lattice]


def test_get_all_surfaces(uo2):
    planes = [openmc.ZPlane(z0=z) for z in np.linspace(-100., 100.)]
    slabs = []
    for region in openmc.model.subdivide(planes):
        slabs.append(openmc.Cell(fill=uo2, region=region))
    geom = openmc.Geometry(slabs)

    surfs = set(geom.get_all_surfaces().values())
    assert not surfs ^ set(planes)


def test_get_by_name():
    m1 = openmc.Material(name='zircaloy')
    m1.add_element('Zr', 1.0)
    m2 = openmc.Material(name='Zirconium')
    m2.add_element('Zr', 1.0)

    s1 = openmc.Sphere(name='surface1')
    c1 = openmc.Cell(fill=m1, region=-s1, name='cell1')
    u1 = openmc.Universe(name='Zircaloy universe', cells=[c1])

    s2 = openmc.ZCylinder(name='surface2')
    c2 = openmc.Cell(fill=u1, region=-s2, name='cell2')
    c3 = openmc.Cell(fill=m2, region=+s2, name='Cell3')
    root = openmc.Universe(name='root Universe', cells=[c2, c3])
    geom = openmc.Geometry(root)

    mats = set(geom.get_materials_by_name('zirc'))
    assert not mats ^ {m1, m2}
    mats = set(geom.get_materials_by_name('zirc', True))
    assert not mats ^ {m1}
    mats = set(geom.get_materials_by_name('zirconium', False, True))
    assert not mats ^ {m2}
    mats = geom.get_materials_by_name('zirconium', True, True)
    assert not mats

    surfaces = set(geom.get_surfaces_by_name('surface'))
    assert not surfaces ^ {s1, s2}
    surfaces = set(geom.get_surfaces_by_name('Surface2', False, True))
    assert not surfaces ^ {s2}
    surfaces = geom.get_surfaces_by_name('Surface2', True, True)
    assert not surfaces

    cells = set(geom.get_cells_by_name('cell'))
    assert not cells ^ {c1, c2, c3}
    cells = set(geom.get_cells_by_name('cell', True))
    assert not cells ^ {c1, c2}
    cells = set(geom.get_cells_by_name('cell3', False, True))
    assert not cells ^ {c3}
    cells = geom.get_cells_by_name('cell3', True, True)
    assert not cells

    cells = set(geom.get_cells_by_fill_name('Zircaloy'))
    assert not cells ^ {c1, c2}
    cells = set(geom.get_cells_by_fill_name('Zircaloy', True))
    assert not cells ^ {c2}
    cells = set(geom.get_cells_by_fill_name('Zircaloy', False, True))
    assert not cells ^ {c1}
    cells = geom.get_cells_by_fill_name('Zircaloy', True, True)
    assert not cells

    univs = set(geom.get_universes_by_name('universe'))
    assert not univs ^ {u1, root}
    univs = set(geom.get_universes_by_name('universe', True))
    assert not univs ^ {u1}
    univs = set(geom.get_universes_by_name('universe', True, True))
    assert not univs


def test_hex_prism():
    hex_prism = openmc.model.HexagonalPrism(edge_length=5.0,
                                            origin=(0.0, 0.0),
                                            orientation='y')
    # clear checks
    assert (0.0, 0.0, 0.0) in -hex_prism
    assert (10.0, 10.0, 10.0) not in -hex_prism
    # edge checks
    assert (0.0, 5.01, 0.0) not in -hex_prism
    assert (0.0, 4.99, 0.0) in -hex_prism

    rounded_hex_prism = openmc.model.HexagonalPrism(edge_length=5.0,
                                                    origin=(0.0, 0.0),
                                                    orientation='y',
                                                    corner_radius=1.0)

    # clear checks
    assert (0.0, 0.0, 0.0) in -rounded_hex_prism
    assert (10.0, 10.0, 10.0) not in -rounded_hex_prism
    # edge checks
    assert (0.0, 5.01, 0.0) not in -rounded_hex_prism
    assert (0.0, 4.99, 0.0) not in -rounded_hex_prism


def test_get_lattice_by_name(cell_with_lattice):
    cells, _, _, lattice = cell_with_lattice
    geom = openmc.Geometry([cells[-1]])

    f = geom.get_lattices_by_name
    assert f('lattice') == [lattice]
    assert f('lattice', True) == []
    assert f('Lattice', True) == [lattice]
    assert f('my lattice', False, True) == [lattice]
    assert f('my lattice', True, True) == []


def test_clone():
    c1 = openmc.Cell()
    c2 = openmc.Cell()
    root = openmc.Universe(cells=[c1, c2])
    geom = openmc.Geometry(root)

    clone = geom.clone()
    root_clone = clone.root_universe

    assert root.id != root_clone.id
    assert not (set(root.cells) & set(root_clone.cells))


def test_determine_paths(cell_with_lattice):
    cells, mats, univ, lattice = cell_with_lattice
    u = openmc.Universe(cells=[cells[-1]])
    geom = openmc.Geometry(u)

    geom.determine_paths()
    assert len(cells[0].paths) == 4
    assert len(cells[1].paths) == 4
    assert len(cells[2].paths) == 1
    assert len(mats[0].paths) == 1
    assert len(mats[-1].paths) == 4

    # Test get_instances
    for i in range(4):
        assert geom.get_instances(cells[0].paths[i]) == i
        assert geom.get_instances(mats[-1].paths[i]) == i


def test_from_xml(run_in_tmpdir, mixed_lattice_model):
    # Export model
    mixed_lattice_model.export_to_xml()

    mats_from_xml = openmc.Materials.from_xml('materials.xml')
    # checking string a Path are both acceptable
    for path in ['geometry.xml', Path('geometry.xml')]:
        for materials in [mats_from_xml, 'materials.xml']:
            # Import geometry from file
            geom = openmc.Geometry.from_xml(path=path, materials=materials)
            assert isinstance(geom, openmc.Geometry)
            ll, ur = geom.bounding_box
            assert ll == pytest.approx((-6.0, -6.0, -np.inf))
            assert ur == pytest.approx((6.0, 6.0, np.inf))

    with pytest.raises(TypeError) as excinfo:
        geom = openmc.Geometry.from_xml(path='geometry.xml', materials=None)
    assert 'Unable to set "materials" to "None"' in str(excinfo.value)

    # checking that the default args also work
    geom = openmc.Geometry.from_xml()
    assert isinstance(geom, openmc.Geometry)
    ll, ur = geom.bounding_box
    assert ll == pytest.approx((-6.0, -6.0, -np.inf))
    assert ur == pytest.approx((6.0, 6.0, np.inf))


def test_rotation_matrix():
    """Test ability to set a rotation matrix directly"""
    y = openmc.YPlane()
    cyl1 = openmc.ZCylinder(r=1.0)
    cyl2 = openmc.ZCylinder(r=2.0, boundary_type='vacuum')

    # Create a universe and then reflect in the y-direction
    c1 = openmc.Cell(region=-cyl1 & +y)
    c2 = openmc.Cell(region=+cyl1 & +y)
    c3 = openmc.Cell(region=-y)
    univ = openmc.Universe(cells=[c1, c2, c3])
    c = openmc.Cell(fill=univ, region=-cyl2)
    c.rotation = [[1, 0, 0], [0, -1, 0], [0, 0, 1]]
    assert np.all(c.rotation_matrix == c.rotation)
    geom = openmc.Geometry([c])

    assert geom.find((0.0, 0.5, 0.0))[-1] == c3
    assert geom.find((0.0, -0.5, 0.0))[-1] == c1
    assert geom.find((0.0, -1.5, 0.0))[-1] == c2

def test_remove_redundant_surfaces():
    """Test ability to remove redundant surfaces"""

    m1 = openmc.Material()
    m1.add_nuclide('U235', 1.0, 'wo')
    m1.add_nuclide('O16', 2.0, 'wo')
    m1.set_density('g/cm3', 10.0)

    m2 = openmc.Material()
    m2.add_element('Zr', 1.0)
    m2.set_density('g/cm3', 2.0)

    m3 = openmc.Material()
    m3.add_nuclide('H1', 2.0)
    m3.add_nuclide('O16', 1.0)
    m3.set_density('g/cm3', 1.0)

    def get_cyl_cell(r1, r2, z1, z2, fill):
        """Create a finite height cylindrical cell with a fill"""

        cyl2 = openmc.ZCylinder(r=r2)
        zplane1 = openmc.ZPlane(z1)
        zplane2 = openmc.ZPlane(z2)
        if np.isclose(r1, 0):
            region = -cyl2 & +zplane1 & -zplane2
        else:
            cyl1 = openmc.ZCylinder(r=r1)
            region = +cyl1 & -cyl2 & +zplane1 & -zplane2
        return openmc.Cell(region=region, fill=fill)

    r1, r2, r3 = 1., 2., 3.
    z1, z2 = -2., 2.
    fuel = get_cyl_cell(0, r1, z1, z2, m1)
    clad = get_cyl_cell(r1, r2, z1, z2, m2)
    water = get_cyl_cell(r2, r3, z1, z2, m3)
    root = openmc.Universe(cells=[fuel, clad, water])
    geom = openmc.Geometry(root=root, merge_surfaces=True, surface_precision=11)
    assert geom.merge_surfaces is True
    geom.merge_surfaces = False
    assert geom.merge_surfaces is False
    assert geom.surface_precision == 11
    geom.surface_precision = 10
    assert geom.surface_precision == 10
    model = openmc.model.Model(geometry=geom,
                               materials=openmc.Materials([m1, m2, m3]))

    # There should be 6 redundant surfaces in this geometry
    n_redundant_surfs = len(geom.remove_redundant_surfaces().keys())
    assert n_redundant_surfs == 6
    # There should be 0 remaining redundant surfaces
    n_redundant_surfs = len(geom.remove_redundant_surfaces().keys())
    assert n_redundant_surfs == 0

def test_get_all_nuclides():
    m1 = openmc.Material()
    m1.add_nuclide('Fe56', 1)
    m1.add_nuclide('Be9', 1)
    m2 = openmc.Material()
    m2.add_nuclide('Be9', 1)
    s = openmc.Sphere()
    c1 = openmc.Cell(fill=m1, region=-s)
    c2 = openmc.Cell(fill=m2, region=+s)
    geom = openmc.Geometry([c1, c2])
    assert geom.get_all_nuclides() == ['Be9', 'Fe56']
