import numpy as np

try:
    import pygmsh
except ImportError:
    pygmsh = None

try:
    import dune
    from dune.fem import adapt, mark, markNeighbors
    from dune.fem.function import gridFunction
    from dune.fem.view import adaptiveLeafGridView, filteredGridView
    from dune.alugrid import aluConformGrid as leafGridView
except ImportError:
    print(
        """
Example code requires dune to run. To install run
pip install dune-fem
    """
    )


from dune.grid import cartesianDomain
from dune.ufl import Constant, Space
from ufl import SpatialCoordinate, sqrt

from ddfem import GridView
from ddfem import geometry as gm
from ddfem.geometry.domain_dune import DomainDune


def getDomain(initialRefine, version, adaptLevels=0, epsFactor=4.5, dirichlet=True):

    # gm.SDF.smoothing = 50

    shiftx, shifty = sqrt(2) * 1e-6, sqrt(3) * 1e-6

    if dirichlet:
        domain_range = [
            [-0.75 + shiftx, -0.75 + shifty, 0.1],  # -0.5
            [0.75 + shiftx, 0.75 + shifty, 3.3],  #  3.2
        ]
        # initial_gridsize = [60, 60, 60]
        initial_gridsize = [25, 25, 25]
    else:
        domain_range = [
            [-0.75 + shiftx, -0.75 + shifty, 0.0 - 0.25],
            [0.75 + shiftx, 0.75 + shifty, 3.2 + 0.25],
        ]
        initial_gridsize = [25, 25, 40]
    initial_gridsize = [i * 2**initialRefine for i in initial_gridsize]
    h = sqrt(
        ((domain_range[1][0] - domain_range[0][0]) / initial_gridsize[0]) ** 2
        + ((domain_range[1][1] - domain_range[0][1]) / initial_gridsize[1]) ** 2
        + ((domain_range[1][2] - domain_range[0][2]) / initial_gridsize[2]) ** 2
    )

    def get_eps(h):
        return Constant(epsFactor * h * 0.5 ** (adaptLevels / 3), "epsilon")

    balls = [
        [0.3, [0.15, 0.15], "b1"],
        [0.3, [-0.15, -0.15], "b2"],
        [0.4, [0, 0], "b3"],
    ]

    b = [gm.Ball(c[0], c[1], name=c[2]) for c in balls]
    sdfs = [b[0] | b[1], b[2]]
    face_2d = sdfs[0] & sdfs[1]

    face_2d.name = "beam"

    if dirichlet and not version == "fitted":
        length = 4
    else:
        length = 3.2
    omega = face_2d.extrude(length, True)
    omega.name = "full"

    h_max = h * 4
    h_min = h / 3
    radius = 0.5    # 3

    x = SpatialCoordinate(Space(3))
    sdf = omega(x)

    def spacing(x, y, z, epsilon):
        r_min = epsilon.value
        r_max = radius * epsilon.value
        dist = np.abs(sdf((x, y, z)))

        gmax = geom.characteristic_length_max
        gmin = geom.characteristic_length_min

        if False:
            # Linear
            if dist <= r_min:
                return gmin
            elif dist >= r_max:
                return gmax
            else:
                m = (gmax - gmin) / (r_max - r_min)
                return m * (dist - r_min) + gmin

        else:
            # Exponential
            R = int(3 * np.log2(gmax / gmin))
            j = max(0, min(R, (dist * R) / r_max - 0.5))
            # j = max(0, min(R, np.floor(dist * R / r_max)))
            return gmax * 0.5 ** ((R - j) / 3)

    omega.epsilon = get_eps(h_min)

    if version == "cartesian":
        omega.epsilon = get_eps(h)
        domain = cartesianDomain(*domain_range, initial_gridsize)

    elif version == "fitted":
        if pygmsh is None:
            raise AttributeError("'fitted' requires install pygmsh")
        with pygmsh.occ.Geometry() as geom:
            geom.characteristic_length_max = h_max / sqrt(2)
            geom.characteristic_length_min = h_min / sqrt(2)

            disks = [geom.add_disk([c[1][0], c[1][1], 0.0], c[0]) for c in balls]

            ds = geom.boolean_union([disks[0], disks[1]])
            shape = geom.boolean_intersection([ds, disks[2]])
            geom.extrude(shape, [0, 0, length])

            geom.set_mesh_size_callback(
                lambda dim, tag, x, y, z, lc: spacing(x, y, z, omega.epsilon),
                ignore_other_mesh_sizes=True,
            )
            mesh = geom.generate_mesh()
            points, cells = mesh.points, mesh.cells_dict
            domain = {
                "vertices": points[:,].astype(float),
                "simplices": cells["tetra"].astype(int),
            }

    elif version == "dune_adaptive":
        gridsize = [int(j * h / h_max) for j in initial_gridsize]
        domain = cartesianDomain(*domain_range, gridsize)

    elif version == "gmsh_adaptive":
        if pygmsh is None:
            raise AttributeError("'gmsh_adaptive' requires install pygmsh")
        with pygmsh.occ.Geometry() as geom:
            geom.characteristic_length_max = h_max / sqrt(2)
            geom.characteristic_length_min = h_min / sqrt(2)

            geom.add_box(
                [0.0, 0.0, 0.0],
                [
                    domain_range[1][0] - domain_range[0][0],
                    domain_range[1][1] - domain_range[0][1],
                    domain_range[1][2] - domain_range[0][2],
                ],
            )

            geom.set_mesh_size_callback(
                lambda dim, tag, x, y, z, lc: spacing(x, y, z, omega.epsilon),
                ignore_other_mesh_sizes=True,
            )
            mesh = geom.generate_mesh()
            points, cells = mesh.points, mesh.cells_dict
            domain = {
                "vertices": points[:,].astype(float),
                "simplices": cells["tetra"].astype(int),
            }
    else:
        raise ValueError("invalid mesh type")

    if version == "dune_adaptive":
        refinements = int(3 * np.log2(h_max / h_min))
        gridView = GridView(
            domain,
            omega,
            factor=radius,
            refinements=refinements,
            adaptLevels=adaptLevels,
            filterTolerance=0.08,

        )
        h_min = h_max * 0.5 ** (refinements/3+1)
        omega.epsilon.value = get_eps(h_min).value
        """
        gridView = adaptiveLeafGridView(leafGridView(domain))
        omega.epsilon = get_eps(h_min)
        omega.epsilon.value *= radius
        epsilon_value = omega.epsilon.value
        marker = mark
        refinements = int(3 * np.log2(h_max / h_min))
        region = gridFunction(
            omega.phi(x) * (1 - omega.phi(x)), gridView=gridView
        )  # interface
        for j in range(1, refinements-1):
            print("refining:", j, gridView.size(3), flush=True)
            # if gridView.size(3) > 1e5:
            #     break
            omega.epsilon.value = epsilon_value * j / refinements
            marker(region, 0.00247262315663, maxLevel=refinements)  # epsilon
            adapt(gridView.hierarchicalGrid)
        h_min = h_max * 0.5 ** (refinements/3)
        omega.epsilon.value = get_eps(h_min).value
        gridView = filteredGridView(gridView, lambda e:1, 1, useFilteredIndexSet=True)
        """
    else:
        gridView = GridView(domain)

    domain = DomainDune(omega, gridView)

    print(
        f"h_max={h_max}, h_min={h_min * 0.5 ** (adaptLevels / 2)}, epsilon={omega.epsilon.value}"
    )

    return gridView, domain
