import numpy as np

try:
    import pygmsh
except ImportError:
    pygmsh = None

try:
    import dune
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, inverted, adaptLevels=0, epsFactor=4.5, smoothing=None
):

    gm.SDF.smoothing = smoothing
    shiftx, shifty = sqrt(2) * 1e-6, sqrt(3) * 1e-6
    domain_range = [[-0.5 + shiftx, -0.5 + shifty], [0.5 + shiftx, 0.5 + shifty]]
    initial_gridsize = [100 * 2**initialRefine] * 2
    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
    )

    def get_eps(h):
        return Constant(epsFactor * h * 0.5 ** (adaptLevels / 2), "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]]
    sdfs[0].name = "sides"
    sdfs[1].name = "ends"
    omega = sdfs[0] & sdfs[1]
    if inverted:
        omega = omega.invert()
    omega.name = "full"

    h_max = h * 3
    h_min = h / 2
    radius = 4

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

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

        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(2 * 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) / 2)

    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]])
    #         if inverted:
    #             rectangle = geom.add_rectangle(
    #                 [domain_range[0][0], domain_range[0][1], 0.0],
    #                 domain_range[1][0] - domain_range[0][0],
    #                 domain_range[1][1] - domain_range[0][1],
    #             )
    #             geom.boolean_difference(rectangle, shape)

    #         geom.set_mesh_size_callback(
    #             lambda dim, tag, x, y, z, lc: spacing(x, y, omega.epsilon),
    #             ignore_other_mesh_sizes=True,
    #         )
    #         mesh = geom.generate_mesh()
    #         points, cells = mesh.points, mesh.cells_dict
    #         domain = {
    #             "vertices": points[:, :2].astype(float),
    #             "simplices": cells["triangle"].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_rectangle(
                [domain_range[0][0], domain_range[0][1], 0.0],
                domain_range[1][0] - domain_range[0][0],
                domain_range[1][1] - domain_range[0][1],
            )

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

    elif version == "gmsh_embedded" or 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]])
            rectangle = geom.add_rectangle(
                [domain_range[0][0], domain_range[0][1], 0.0],
                domain_range[1][0] - domain_range[0][0],
                domain_range[1][1] - domain_range[0][1],
            )

            geom.boolean_fragments(rectangle, shape)

            geom.add_physical(shape, label="fitted_domain")

            geom.set_mesh_size_callback(
                lambda dim, tag, x, y, z, lc: spacing(x, y, omega.epsilon),
                ignore_other_mesh_sizes=True,
            )
            mesh = geom.generate_mesh()
            points, cells = mesh.points, mesh.cells_dict

            if version == "fitted":
                fitted_domain_indices = mesh.cell_sets_dict["fitted_domain"]["triangle"]
                mesh_cells = cells["triangle"][fitted_domain_indices].astype(int)
            else:
                mesh_cells = cells["triangle"].astype(int)

            domain = {
                "vertices": points[:, :2].astype(float),
                "simplices": mesh_cells,
            }

    else:
        raise ValueError("invalid mesh type")

    if version == "dune_adaptive":
        refinements = int(2 * np.log2(h_max / h_min))
        gridView = GridView(
            domain,
            omega,
            filterTolerance=10 * omega.epsilon.value,
            factor=radius,
            refinements=refinements,
            adaptLevels=adaptLevels,
        )
        h_min = h_max * 0.5 ** (refinements / 2)
        omega.epsilon = get_eps(h_min)
    else:
        gridView = GridView(
            domain,
            omega,
            filterTolerance=10 * omega.epsilon.value,
        )

    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
