import copy
from typing import Any, Dict, Union

import numpy as np
from skfem import MeshTri1


class Domain:
    """Square domain [0,1]x[0,1] with initial coarse mesh and fine integration mesh."""

    def __init__(
        self,
        *,
        domain_config: Dict[Union[str, int], Any],
        random_state: np.random.RandomState,
    ):
        xmin, ymin, xmax, ymax = domain_config.get("boundary", [0.0, 0.0, 1.0, 1.0])
        self._boundary = np.array([xmin, ymin, xmax, ymax])
        self._random_state = random_state

        num_elements = domain_config.get("initial_num_elements", None)
        if num_elements is not None:
            domain_area = (xmax - xmin) * (ymax - ymin)
            self._max_volume = 2.0 * domain_area / float(num_elements)
        else:
            self._max_volume = domain_config.get("max_initial_element_volume", 0.05)

        self._initial_mesh = self._create_initial_mesh()

    @property
    def initial_mesh(self) -> MeshTri1:
        return copy.deepcopy(self._initial_mesh)

    def replace_initial_mesh(self, mesh: MeshTri1) -> None:
        """Replace the stored initial mesh (e.g. after Nyquist enforcement)."""
        self._initial_mesh = mesh

    def get_integration_mesh(self) -> MeshTri1:
        return self._initial_mesh.refined(4)

    @property
    def boundary_line_segments(self) -> np.ndarray:
        boundary_edges = self._initial_mesh.boundary_facets()
        boundary_node_indices = self._initial_mesh.facets[:, boundary_edges]
        return self._initial_mesh.p[:, boundary_node_indices].T.reshape(-1, 4)

    def _create_initial_mesh(self) -> MeshTri1:
        return self._meshpy_square()

    def _meshpy_square(self) -> MeshTri1:
        import meshpy.triangle as triangle

        xmin, ymin, xmax, ymax = self._boundary
        points = [(xmin, ymin), (xmax, ymin), (xmax, ymax), (xmin, ymax)]
        facets = [(0, 1), (1, 2), (2, 3), (3, 0)]

        info = triangle.MeshInfo()
        info.set_points(points)
        info.set_facets(facets)

        mesh = triangle.build(info, max_volume=self._max_volume)
        vertices = np.array(mesh.points).T
        triangles = np.array(mesh.elements).T
        return MeshTri1(vertices, triangles)


def create_domain(
    *, domain_config: Dict[Union[str, int], Any], random_state: np.random.RandomState
) -> Domain:
    return Domain(domain_config=domain_config, random_state=random_state)
