# fichier : cgal_pycad.py
from fractions import Fraction
from typing import List, Optional, Union
try:
    from . import _cgal_pycad
except ImportError:
    # Fallback for local development if .so is in the same directory but not installed as package
    import _cgal_pycad

# ==============================
# Utilitaires de conversion exactes
# ==============================
def _to_fraction(val: Union[int, Fraction, tuple, float]) -> Fraction:
    if isinstance(val, Fraction):
        return val
    elif isinstance(val, int):
        return Fraction(val)
    elif isinstance(val, tuple) and len(val) == 2:
        num, denom = val
        return Fraction(num, denom)
    elif isinstance(val, float):
        raise TypeError("Floats are strictly forbidden in exact mode. Use Fraction or (num, den).")
    else:
        raise TypeError(f"Unsupported type {type(val)}; must be int, Fraction, or (num, denom)")

def _to_exact_str(val: Union[int, Fraction, tuple]) -> str:
    f = _to_fraction(val)
    return f"{f.numerator}/{f.denominator}"

def _to_tolerance_str(val: Union[int, Fraction, tuple, float]) -> str:
    if isinstance(val, float):
        f = Fraction(val).limit_denominator(1000000)
        return f"{f.numerator}/{f.denominator}"
    return _to_exact_str(val)

def _to_fraction_list(vals: List[Union[int, Fraction, tuple]]) -> List[Fraction]:
    return [_to_fraction(v) for v in vals]

def _to_exact_str_list(vals: List[Union[int, Fraction, tuple]]) -> List[str]:
    return [_to_exact_str(v) for v in vals]

# ==============================
# Classe wrapper Solid
# ==============================
class Solid:
    """
    Wrapper class for CGAL exact Nef Polyhedrons.
    
    This class represents a 3D solid using exact rational arithmetic. It is immutable;
    transformations and boolean operations return new Solid instances.
    
    Attributes:
        _solid: The underlying C++ object (implementation detail).
    """

    def __init__(self, base: _cgal_pycad.Solid = None):
        """
        Initialize a Solid. Users should generally use primitive functions (cube, sphere) 
        instead of calling this directly.

        Args:
            base: Internal C++ Solid object. If None, creates an empty solid.
        """
        if base is None:
            self._solid = _cgal_pycad.Solid()
        else:
            self._solid = base

    # ------------------------
    # Transformations exactes
    # ------------------------
    def translate(self, vec: List[Union[int, Fraction, tuple]]) -> "Solid":
        """
        Translate the solid by a vector.

        Args:
            vec: A 3-element list/tuple [dx, dy, dz]. Elements must be exact types
                 (int, Fraction, or (num, den) tuple). Floats are forbidden.

        Returns:
            A new translated Solid instance.

        Raises:
            ValueError: If vec is not length 3.
            TypeError: If elements are floats.
        
        Example:
            >>> s = cube([10, 10, 10])
            >>> s_moved = s.translate([5, 0, 0])
        """
        if len(vec) != 3: raise ValueError("Translation vector must be size 3")
        strs = _to_exact_str_list(vec)
        return Solid(self._solid.translate(strs[0], strs[1], strs[2]))

    def rotate(self, axis: List[Union[int, Fraction, tuple]], angle: Union[int, Fraction, tuple, float]) -> "Solid":
        """
        Rotate the solid around an axis by an angle.

        Args:
            axis: A 3-element vector [x, y, z] defining the axis of rotation.
            angle: The rotation angle in degrees. Can be exact (int, Fraction) or 
                   float (automatically converted to high-precision rational).

        Returns:
            A new rotated Solid instance. The result is a valid rational Nef Polyhedron,
            approximating the rotation matrix to high precision if necessary.

        Raises:
            ValueError: If axis is not length 3.
        
        Example:
            >>> s = cube([10, 10, 10])
            >>> # Rotate 45 degrees around Z axis
            >>> s_rot = s.rotate([0, 0, 1], 45)
        """
        if len(axis) != 3: raise ValueError("Rotation axis must be size 3")
        ax_strs = _to_exact_str_list(axis)
        ang_str = _to_tolerance_str(angle)
        return Solid(self._solid.rotate(ax_strs[0], ax_strs[1], ax_strs[2], ang_str))

    def mirror(self, plane: List[Union[int, Fraction, tuple]]) -> "Solid":
        """
        Mirror the solid across a plane defined by ax + by + cz + d = 0.

        Args:
            plane: A 4-element list [a, b, c, d].

        Returns:
            A new mirrored Solid instance.

        Raises:
            ValueError: If plane is not length 4.
        """
        if len(plane) != 4: raise ValueError("Plane must be defined by 4 coefficients [a,b,c,d]")
        strs = _to_exact_str_list(plane)
        return Solid(self._solid.mirror(strs[0], strs[1], strs[2], strs[3]))

    def scale(self, vec: List[Union[int, Fraction, tuple]]) -> "Solid":
        """
        Scale the solid by factors along X, Y, Z.

        Args:
            vec: A 3-element list [sx, sy, sz].

        Returns:
            A new scaled Solid instance.
        """
        if len(vec) != 3: raise ValueError("Scale vector must be size 3")
        strs = _to_exact_str_list(vec)
        return Solid(self._solid.scale(strs[0], strs[1], strs[2]))

    def resize(self, x: Optional[Union[int, Fraction, tuple]]=None,
                     y: Optional[Union[int, Fraction, tuple]]=None,
                     z: Optional[Union[int, Fraction, tuple]]=None):
        """
        Resize the solid to specific dimensions (Bounding Box).
        
        **Not Implemented**: Currently issues a warning and returns self.

        Args:
            x: Target size in X.
            y: Target size in Y.
            z: Target size in Z.

        Returns:
            self (unchanged).
        """
        print("Warning: resize() not fully implemented in backend yet.")
        return self

    # ------------------------
    # Booléens exacts
    # ------------------------
    def union(self, other: "Solid") -> "Solid":
        """
        Compute the boolean union of this solid and another.

        Args:
            other: The other Solid to unite with.

        Returns:
            A new Solid representing the union.
        """
        return Solid(self._solid.union(other._solid))

    def intersect(self, other: "Solid") -> "Solid":
        """
        Compute the boolean intersection of this solid and another.

        Args:
            other: The other Solid to intersect with.

        Returns:
            A new Solid representing the intersection.
        """
        return Solid(self._solid.intersect(other._solid))

    def difference(self, other: "Solid") -> "Solid":
        """
        Compute the boolean difference (self - other).

        Args:
            other: The Solid to subtract.

        Returns:
            A new Solid representing the difference.
        """
        return Solid(self._solid.difference(other._solid))

    # ------------------------
    # Surcharge d’opérateurs
    # ------------------------
    def __add__(self, other: "Solid"):
        """Operator overload for union (self + other)."""
        return self.union(other)
    
    def __mul__(self, other: "Solid"): 
        """Operator overload for intersection (self * other)."""
        return self.intersect(other)
    
    def __sub__(self, other: "Solid"): 
        """Operator overload for difference (self - other)."""
        return self.difference(other)

    # ------------------------
    # Export
    # ------------------------
    def to_stl(self, path: str):
        """
        Export the solid to an STL file.

        Args:
            path: Target file path (e.g., "output.stl").
        """
        self._solid.to_stl(path)

    def to_off(self, path: str):
        """
        Export the solid to an OFF file.

        Args:
            path: Target file path (e.g., "output.off").
        """
        self._solid.to_off(path)

# ==============================
# Primitives OpenSCAD-style
# ==============================
def cube(size: List[Union[int, Fraction, tuple]], center=False) -> Solid:
    """
    Create a cube (rectangular algorithm).

    Args:
        size: [x, y, z] dimensions. Must be strict exact types (int, Fraction).
        center: If True, the cube is centered at the origin. Otherwise, it starts at [0,0,0] extends to +x,+y,+z.

    Returns:
        A Solid representing the cube.

    Example:
        >>> c = cube([10, 20, 5], center=True)
    """
    if len(size) != 3: raise ValueError("Cube size must be [x,y,z]")
    strs = _to_exact_str_list(size)
    return Solid(_cgal_pycad.cube(strs[0], strs[1], strs[2], center))

def sphere(r: Union[int, Fraction, tuple], tolerance: Union[int, Fraction, tuple, float]=Fraction(1,1000)) -> Solid:
    """
    Create a sphere approximation using a convex hull of points.

    Args:
        r: Radius. Strict exact type.
        tolerance: Maximum distance error from ideal sphere surface. 
                   Floats allowed here (convenience). Controls resolution.
                   Smaller tolerance = more vertices = higher quality.

    Returns:
        A Solid representing the polyhedral sphere.
    """
    r_str = _to_exact_str(r)
    tol_str = _to_tolerance_str(tolerance)
    return Solid(_cgal_pycad.sphere(r_str, tol_str))

def cylinder(r: Union[int, Fraction, tuple], h: Union[int, Fraction, tuple],
             tolerance: Union[int, Fraction, tuple, float]=Fraction(1,1000), center=False) -> Solid:
    """
    Create a cylinder approximation.

    Args:
        r: Radius.
        h: Height.
        tolerance: Error tolerance for circular approximation.
        center: If True, centered along Z (from -h/2 to h/2). If False, from 0 to h.

    Returns:
        A Solid representing the cylinder.
    """
    r_str = _to_exact_str(r)
    h_str = _to_exact_str(h)
    tol_str = _to_tolerance_str(tolerance)
    return Solid(_cgal_pycad.cylinder(r_str, h_str, tol_str, center))

def cone(r1: Union[int, Fraction, tuple], r2: Union[int, Fraction, tuple], h: Union[int, Fraction, tuple],
         tolerance: Union[int, Fraction, tuple, float]=Fraction(1,1000), center=False) -> Solid:
    """
    Create a cone (truncated).

    Args:
        r1: Bottom radius.
        r2: Top radius.
        h: Height.
        tolerance: Error tolerance.
        center: If True, centered along Z.

    Returns:
        A Solid representing the cone.
    """
    r1_str = _to_exact_str(r1)
    r2_str = _to_exact_str(r2)
    h_str = _to_exact_str(h)
    tol_str = _to_tolerance_str(tolerance)
    return Solid(_cgal_pycad.cone(r1_str, r2_str, h_str, tol_str, center))

# ==============================
# Functional Wrappers (DSL)
# ==============================

class Modifier:
    """Helper for functional-style transformations (e.g. translate(v)(obj))."""
    def __init__(self, func, *args):
        self.func = func
        self.args = args
    def __call__(self, solid: Solid) -> Solid:
        return getattr(solid, self.func)(*self.args)

def translate(vec: List[Union[int, Fraction, tuple]]) -> Modifier:
    """
    Functional wrapper for translation.

    Usage:
        >>> obj = cube([10,10,10])
        >>> moved = translate([5,0,0])(obj)
    """
    return Modifier("translate", vec)

def rotate(axis: List[Union[int, Fraction, tuple]], angle: Union[int, Fraction, tuple, float]) -> Modifier:
    """
    Functional wrapper for rotation.

    Usage:
        >>> obj = cube([10,10,10])
        >>> rotated = rotate([0,0,1], 45)(obj)
    """
    return Modifier("rotate", axis, angle)

def scale(vec: List[Union[int, Fraction, tuple]]) -> Modifier:
    """Functional wrapper for scaling."""
    return Modifier("scale", vec)

def mirror(plane: List[Union[int, Fraction, tuple]]) -> Modifier:
    """Functional wrapper for mirroring."""
    return Modifier("mirror", plane)

def resize(x=None, y=None, z=None) -> Modifier:
    """Functional wrapper for resizing (Not Implemented)."""
    return Modifier("resize", x, y, z)
