#
# Claudio Perez
#
from dataclasses import dataclass
from collections import defaultdict

from .torsion import TorsionAnalysis
import numpy as np


class FlexureAnalysis:
    pass

@dataclass
class _Element:
    nodes: tuple # of int
  # gauss: tuple # of Gauss
    shape: str
    model: dict = None


class PlaneMesh:
    def __init__(self, nodes, elems):#, offset=None):
        self.nodes = nodes 
        self.elems = elems
        # self.offset = offset

    def cells(self):
        return [
            elem.nodes for elem in self.elems
        ]

    def translate(self, offset):
        return type(self)(self.nodes-np.array(offset), self.elems)#, self.offset)

    def rotate(self, angle):
        R = np.array([[np.cos(angle), -np.sin(angle)],
                      [np.sin(angle), np.cos(angle)]])
        return type(self)((R@self.nodes.T).T, self.elems)#, self.offset)


class TriangleModel(PlaneMesh):
    def __init__(self, *args, **kwds):
        super().__init__(*args, **kwds)

        for i,elem in enumerate(self.elems):
            assert len(elem.nodes) == 3
            if self.cell_area(i) < 0:
                elem.nodes = elem.nodes[::-1]

    @classmethod
    def from_meshio(cls, mesh, **kwds):
        # meshio object, assume all tri3s
        nodes = mesh.points
        cells = None
        if nodes.shape[1] == 3:
            nodes = nodes[:, :2]
        for cells in mesh.cells:
            if cells.type == "triangle":
                break

        if cells is None:
            raise ValueError("No triangle cells found in mesh")

        elems = [
            _Element(nodes=cell, shape="T3") for cell in cells.data
        ]
        return TriangleModel(nodes, elems)

    def create_handle(self, state):
        """
        Returns a function handle that approximates the solution at any (x,y)
        by performing piecewise linear interpolation over the mesh.

        Assumes that self.state is a numpy array of nodal values.
        """

        def u(point):
            point = np.array(point)
            # Loop over all triangles (elements) to locate the point.
            for elem in self.elems:
                indices = elem.nodes  # indices into self.nodes and state
                # Get the triangle's vertices
                A, B, C = self.nodes[indices]
                # Compute barycentric coordinates for point P with respect to triangle ABC.
                v0 = B - A
                v1 = C - A
                v2 = point - A

                d00 = np.dot(v0, v0)
                d01 = np.dot(v0, v1)
                d11 = np.dot(v1, v1)
                d20 = np.dot(v2, v0)
                d21 = np.dot(v2, v1)

                denom = d00 * d11 - d01 * d01
                if denom == 0:
                    # Degenerate triangle; skip it.
                    continue
                a = (d11 * d20 - d01 * d21) / denom
                b = (d00 * d21 - d01 * d20) / denom
                c = 1 - a - b

                # Check if the point lies inside the triangle (with a tolerance).
                tol = 1e-12
                if a >= -tol and b >= -tol and c >= -tol:
                    # Interpolate the solution using barycentric coordinates.
                    return a * state[indices[0]] + b * state[indices[1]] + c * state[indices[2]]

            # If the point is not found in any triangle, raise an error.
            x, y = point
            raise ValueError(f"Point ({x}, {y}) is not inside any triangle in the mesh.")

        return u

    def exterior(self):
        """
        Returns a list of (x, y) nodes that form the exterior boundary.
        """
        cycles = _boundary_cycles(self.cells())
        # Convert cycles of node indices to cycles of (x, y) coordinates.
        cycles_pts = [[self.nodes[i] for i in cyc] for cyc in cycles if len(cyc) >= 3]
        if not cycles_pts:
            return []

        # The cycle with the largest absolute area is assumed to be the exterior.
        areas = [abs(_signed_area(pts)) for pts in cycles_pts]
        exterior_cycle = cycles_pts[areas.index(max(areas))]
        # # exterior_cycle.append(exterior_cycle[0])
        from veux.utility.alpha_shape import remove_collinear_points
        return remove_collinear_points(np.array(exterior_cycle))
        # return np.array(exterior_cycle) #[:-1,:]

    def interior(self):
        """
        Returns a list of lists, each inner list is a list of (x, y) nodes
        defining a hole in the mesh.
        """
        cycles = _boundary_cycles(self.cells())
        cycles_pts = [[self.nodes[i] for i in cyc] for cyc in cycles if len(cyc) >= 3]
        if not cycles_pts:
            return []

        areas = [abs(_signed_area(pts)) for pts in cycles_pts]
        # Assume the cycle with the maximum area is the exterior.
        ext_index = areas.index(max(areas))
        holes = [pts for i, pts in enumerate(cycles_pts) if i != ext_index]
        return holes


    def cell_area(self, tag=None)->float:
        if tag is None:
            return sum(self.cell_area(i) for i in range(len(self.elems)))

        y, z = self.nodes[self.elems[tag].nodes].T
        z1, z2, z3 = z
        y1, y2, y3 = y
        return -float(0.5 * ((y2 - y1) * (z3 - z1) - (y3 - y1) * (z2 - z1)))

    def cell_solution(self, tag, state):
        return float(sum(state[self.elems[tag].nodes]))/3

    def cell_gradient(self, tag, field):
        # May also try Cook equation 3.4-6
        u1, u2, u3 = field[self.elems[tag].nodes]
        ((y1, y2, y3), (z1, z2, z3)) = self.nodes[self.elems[tag].nodes].T
        z12 = z1 - z2
        z23 = z2 - z3
        z31 = z3 - z1
        y32 = y3 - y2
        y13 = y1 - y3
        y21 = y2 - y1
        A = self.cell_area(tag)
        return 1/(2*A)*np.array([
            z23*u1 + z31*u2 + z12*u3,
            y32*u1 + y13*u2 + y21*u3
        ])

    def energy(self, u, v):
        q = 0.0
        for elem in self.elems:
            ((y1, y2, y3), (z1, z2, z3)) = self.nodes[elem.nodes].T

            z12 = z1 - z2
            z23 = z2 - z3
            z31 = z3 - z1
            y32 = y3 - y2
            y13 = y1 - y3
            y21 = y2 - y1

            area = -0.5 * ((y2 - y1) * (z3 - z1) - (y3 - y1) * (z2 - z1))

            k11 = ( y32**2 +  z23**2)
            k12 = (y13*y32 + z23*z31)
            k13 = (y21*y32 + z12*z23)
            k22 = ( y13**2 +  z31**2)
            k23 = (y13*y21 + z12*z31)
            k33 = ( y21**2 +  z12**2)

            ke = 1/(4.0*area)*np.array([[k11, k12, k13],
                                        [k12, k22, k23],
                                        [k13, k23, k33]])
            ue = u[elem.nodes]
            ve = v[elem.nodes]
            q += ue.dot(ke@ve)

        return q


    def inner(self, u, v):
        pass

    def curl(self, u, v):

        q = 0
        for elem in self.elems:
            ((y1, y2, y3), (z1, z2, z3)) = self.nodes[elem.nodes].T
            z12 = z1 - z2
            z23 = z2 - z3
            z31 = z3 - z1
            y32 = y3 - y2
            y13 = y1 - y3
            y21 = y2 - y1

            ((y1, y2, y3), (z1, z2, z3)) = u[elem.nodes].T

            f = 1/6.*np.array([
                ((y1*y32 - z1*z23) + (y2*y32 - z2*z23) + (y3*y32 - z3*z23)),
                ((y1*y13 - z1*z31) + (y2*y13 - z2*z31) + (y3*y13 - z3*z31)),
                ((y1*y21 - z1*z12) + (y2*y21 - z2*z12) + (y3*y21 - z3*z12))])

            q += f.dot(v[elem.nodes])

        return q

    def inertia(self, va, ua):
        r"""
        v.dot( ([\int N.T rho @ N dA] @u)
        """
        q = 0.0
        for i,elem in enumerate(self.elems):
            v1, v2, v3 = va[elem.nodes]
            u1, u2, u3 = ua[elem.nodes]
            area = self.cell_area(i)
            # v[nodes].dot(int(N.T@N)@u[nodes])
            q += area/12.0*(u1*(2*v1+v2+v3) + u2*(v1+2*v2+v3) + u3*(v1+v2+2*v3))

        return float(q)


def _boundary_cycles(elems):
    """
    Given a list of triangles (each as a 3-tuple of node indices),
    returns a list of cycles (each cycle is a list of node indices)
    corresponding to the boundary edges.
    """
    # Count how many triangles share each edge.
    edge_count = defaultdict(int)
    for tri in elems:
        for a, b in [(tri[0], tri[1]),
                     (tri[1], tri[2]),
                     (tri[2], tri[0])]:
            key = tuple(sorted((a, b)))
            edge_count[key] += 1

    # Boundary edges are those that appear only once.
    boundary_edges = [edge for edge, count in edge_count.items() if count == 1]

    # Build an undirected graph from boundary edges.
    adj = defaultdict(list)
    for a, b in boundary_edges:
        adj[a].append(b)
        adj[b].append(a)

    # Walk the graph to extract closed cycles.
    cycles = []
    visited = set()
    for start in list(adj.keys()):
        if start in visited:
            continue

        cycle = []
        current = start
        prev = None
        while True:
            cycle.append(current)
            visited.add(current)
            neighbors = adj[current]
            if not neighbors:
                break  # In a well-formed mesh, this shouldn't happen.
            # Pick the neighbor that is not the one we came from.
            if prev is None:
                nxt = neighbors[0]
            else:
                nxt = neighbors[0] if neighbors[0] != prev else (neighbors[1] if len(neighbors) > 1 else neighbors[0])
            if nxt == start:
                break
            prev, current = current, nxt
        cycles.append(cycle)
    return cycles

def _signed_area(pts):
    """
    Computes the signed area of a polygon defined by the list of points.
    """
    area = 0
    n = len(pts)
    for i in range(n):
        x0, y0 = pts[i]
        x1, y1 = pts[(i + 1) % n]
        area += (x0 * y1 - y0 * x1)
    return area / 2

# ------------------------------------------------------------------------
# The following Python code is implemented by Professor Terje Haukaas at
# the University of British Columbia in Vancouver, Canada. It is made
# freely available online at terje.civil.ubc.ca together with notes,
# examples, and additional Python code. Please be cautious when using
# this code; it may contain bugs and comes without warranty of any form.
# ------------------------------------------------------------------------
# https://gist.githubusercontent.com/terjehaukaas/f633c4afc001badb4473d422ccc146e7/raw/2e7d09dbc850dc800c60e1751fb21f2f76615509/SolidCrossSectionAnalysis.py
# https://civil-terje.sites.olt.ubc.ca/files/2020/02/Screenshot-Solid-Cross-section.pdf
#
