"""
JITTERBUG FAMILY GENERATOR FOR REVIT (Dynamo Python Node)
=========================================================

KINEMATIC PRINCIPLES:
- ALL 24 edges = 6" (CONSTANT, never changes)
- 8 TRIANGULAR faces = STRUCTURAL (rigid, 60° angles)
- 6 SQUARE faces = MOBILE (become RHOMBI, angles change)
- 12 vertices connected by rigid 6" bars

TRANSFORMATION PARAMETER: Rhombus Acute Angle
- 90° = Vector Equilibrium (squares)
- 60° = Icosahedron (rhombi at 60°/120°)
- 0°  = Octahedron (collapsed)

4 TRIANGLE AXES (through opposite triangle pairs):
- Axis A [1,1,1]
- Axis B [1,1,-1]
- Axis C [-1,1,1]
- Axis D [-1,1,-1]

USAGE IN DYNAMO:
1. Create a Python Script node
2. Paste this code
3. Connect a Number Slider (0-90) to IN[0] for rhombus_acute_angle
4. Connect a Number input to IN[1] for edge_length (default 6)
5. Output: Geometry for DirectShape or Adaptive Component

Author: Generated for Maroun's Jitterbug research
"""

import clr
import math

clr.AddReference('ProtoGeometry')
from Autodesk.DesignScript.Geometry import *

# =============================================================================
# INPUT PARAMETERS
# =============================================================================

rhombus_acute_angle = IN[0] if IN[0] is not None else 90  # degrees (90=VE, 60=Icosa, 0=Octa)
edge_length = IN[1] if IN[1] is not None else 6  # inches (CONSTANT)

# =============================================================================
# JITTERBUG GEOMETRY CONSTANTS
# =============================================================================

# 8 Triangle face normals (octant directions)
TRIANGLE_OCTANTS = [
    (1, 1, 1),    # T0 +++
    (1, 1, -1),   # T1 ++-
    (-1, 1, 1),   # T2 -++
    (-1, 1, -1),  # T3 -+-
    (1, -1, 1),   # T4 +-+
    (1, -1, -1),  # T5 +--
    (-1, -1, 1),  # T6 --+
    (-1, -1, -1), # T7 ---
]

# 8 Triangular faces (vertex indices) - STRUCTURAL
TRIANGLES = [
    (0, 4, 8),   # T0
    (0, 9, 5),   # T1
    (2, 8, 6),   # T2
    (2, 7, 9),   # T3
    (1, 10, 4),  # T4
    (1, 5, 11),  # T5
    (3, 6, 10),  # T6
    (3, 11, 7),  # T7
]

# 6 Square/Rhombus faces (vertex indices) - MOBILE
SQUARES = [
    (0, 4, 1, 5),    # S0 +X face
    (2, 7, 3, 6),    # S1 -X face
    (0, 9, 2, 8),    # S2 +Y face
    (1, 10, 3, 11),  # S3 -Y face
    (4, 8, 6, 10),   # S4 +Z face
    (5, 11, 7, 9),   # S5 -Z face
]

# Which two triangles share each vertex
VERTEX_TRIANGLE_PAIRS = [
    (0, 1),  # V0
    (4, 5),  # V1
    (2, 3),  # V2
    (6, 7),  # V3
    (0, 4),  # V4
    (1, 5),  # V5
    (2, 6),  # V6
    (3, 7),  # V7
    (0, 2),  # V8
    (1, 3),  # V9
    (4, 6),  # V10
    (5, 7),  # V11
]

# 24 Edges (vertex pairs)
EDGES = [
    # Triangle edges (teal/structural)
    (0, 4), (4, 8), (8, 0),     # T0
    (0, 9), (9, 5), (5, 0),     # T1
    (2, 8), (8, 6), (6, 2),     # T2
    (2, 7), (7, 9), (9, 2),     # T3
    (1, 10), (10, 4), (4, 1),   # T4
    (1, 5), (5, 11), (11, 1),   # T5
    (3, 6), (6, 10), (10, 3),   # T6
    (3, 11), (11, 7), (7, 3),   # T7
]

# Remove duplicates from edges
UNIQUE_EDGES = list(set(tuple(sorted(e)) for e in EDGES))

# =============================================================================
# JITTERBUG TRANSFORMATION FUNCTIONS
# =============================================================================

def normalize(v):
    """Normalize a 3D vector."""
    length = math.sqrt(v[0]**2 + v[1]**2 + v[2]**2)
    if length < 0.0001:
        return (0, 0, 0)
    return (v[0]/length, v[1]/length, v[2]/length)

def cross(a, b):
    """Cross product of two 3D vectors."""
    return (
        a[1]*b[2] - a[2]*b[1],
        a[2]*b[0] - a[0]*b[2],
        a[0]*b[1] - a[1]*b[0]
    )

def dot(a, b):
    """Dot product of two 3D vectors."""
    return a[0]*b[0] + a[1]*b[1] + a[2]*b[2]

def subtract(a, b):
    """Subtract vector b from a."""
    return (a[0]-b[0], a[1]-b[1], a[2]-b[2])

def add(a, b):
    """Add two vectors."""
    return (a[0]+b[0], a[1]+b[1], a[2]+b[2])

def scale(v, s):
    """Scale a vector by scalar s."""
    return (v[0]*s, v[1]*s, v[2]*s)

def compute_jitterbug_vertices(rhombus_angle_deg, L):
    """
    Compute the 12 vertex positions for the Jitterbug at a given rhombus angle.
    
    Parameters:
    - rhombus_angle_deg: Acute angle of rhombus faces (90=VE, 60=Icosa, 0=Octa)
    - L: Edge length (constant, e.g., 6 inches)
    
    Returns:
    - List of 12 (x, y, z) tuples
    """
    
    theta = rhombus_angle_deg * math.pi / 180
    
    # For equilateral triangle with side L:
    # Centroid to vertex distance
    tri_radius = L / math.sqrt(3)
    
    # At VE, triangle centroids are at distance d0 from origin
    d0 = L * math.sqrt(2.0/3.0)
    
    # Rotation angle phi of triangles around their normals
    # Derived from rhombus angle constraint
    # At rhombus_angle = 90°: phi = 0
    # At rhombus_angle = 60°: phi ≈ 22°
    # At rhombus_angle = 0°:  phi = 60°
    
    # The relationship: cos(rhombus_angle/2) relates to the transformation
    # phi = arccos(sin(theta/2) / sin(45°)) approximately
    
    # Simplified: phi proportional to (90 - rhombus_angle)
    phi = (90 - rhombus_angle_deg) * math.pi / 180
    
    # Centroid distance from origin decreases as triangles rotate inward
    centroid_dist = d0 * math.cos(phi)
    
    # Compute triangle centroids
    triangle_centroids = []
    for octant in TRIANGLE_OCTANTS:
        n = normalize(octant)
        centroid = scale(n, centroid_dist)
        triangle_centroids.append(centroid)
    
    # Compute vertex positions for each triangle
    # Each triangle has 3 vertices at tri_radius from centroid,
    # rotated by phi around the normal, at 120° apart
    
    triangle_vertex_positions = []  # [tri_idx][local_vert_idx] = (x,y,z)
    
    for t in range(8):
        normal = normalize(TRIANGLE_OCTANTS[t])
        centroid = triangle_centroids[t]
        
        # Create local coordinate system (u, v perpendicular to normal)
        u = (1, 0, 0)
        if abs(dot(normal, u)) > 0.9:
            u = (0, 1, 0)
        
        # Gram-Schmidt orthogonalization
        u = subtract(u, scale(normal, dot(normal, u)))
        u = normalize(u)
        v = cross(normal, u)
        v = normalize(v)
        
        # Three vertices at 120° apart, rotated by phi
        verts = []
        for i in range(3):
            angle = phi + (i * 2 * math.pi / 3)
            local_x = math.cos(angle) * tri_radius
            local_y = math.sin(angle) * tri_radius
            
            # Position in global coordinates
            pos = add(centroid, add(scale(u, local_x), scale(v, local_y)))
            verts.append(pos)
        
        triangle_vertex_positions.append(verts)
    
    # Match triangle vertices to global vertex indices
    # Each global vertex is shared by exactly 2 triangles
    
    vertices = []
    
    for vi in range(12):
        t1, t2 = VERTEX_TRIANGLE_PAIRS[vi]
        
        # Find closest pair of local vertices between the two triangles
        best_pos = None
        best_dist = float('inf')
        
        for li1 in range(3):
            for li2 in range(3):
                p1 = triangle_vertex_positions[t1][li1]
                p2 = triangle_vertex_positions[t2][li2]
                
                diff = subtract(p1, p2)
                dist = math.sqrt(dot(diff, diff))
                
                if dist < best_dist:
                    best_dist = dist
                    # Average the two positions
                    best_pos = scale(add(p1, p2), 0.5)
        
        vertices.append(best_pos)
    
    return vertices

def classify_edge(v1, v2):
    """
    Classify an edge as STRUCTURAL (triangle) or MOBILE (rhombus).
    Returns 'triangle' or 'rhombus'.
    """
    for tri in TRIANGLES:
        if v1 in tri and v2 in tri:
            return 'triangle'
    return 'rhombus'

# =============================================================================
# GENERATE GEOMETRY
# =============================================================================

# Compute vertices at the specified rhombus angle
vertices = compute_jitterbug_vertices(rhombus_acute_angle, edge_length)

# Convert to Dynamo Points
dynamo_points = [Point.ByCoordinates(v[0], v[1], v[2]) for v in vertices]

# Create edges as Lines
edge_lines = []
triangle_edges = []
rhombus_edges = []

for e in UNIQUE_EDGES:
    v1, v2 = e
    line = Line.ByStartPointEndPoint(dynamo_points[v1], dynamo_points[v2])
    edge_lines.append(line)
    
    if classify_edge(v1, v2) == 'triangle':
        triangle_edges.append(line)
    else:
        rhombus_edges.append(line)

# Create triangular faces (STRUCTURAL)
triangle_surfaces = []
for tri in TRIANGLES:
    a, b, c = tri
    try:
        surf = Surface.ByPerimeterPoints([
            dynamo_points[a], 
            dynamo_points[b], 
            dynamo_points[c]
        ])
        triangle_surfaces.append(surf)
    except:
        pass  # Skip if surface creation fails

# Create rhombus faces (MOBILE)
rhombus_surfaces = []
for sq in SQUARES:
    a, b, c, d = sq
    try:
        surf = Surface.ByPerimeterPoints([
            dynamo_points[a], 
            dynamo_points[b], 
            dynamo_points[c], 
            dynamo_points[d]
        ])
        rhombus_surfaces.append(surf)
    except:
        pass  # Skip if surface creation fails

# Calculate actual rhombus angles for verification
def calc_angle(p1, p2, p3):
    """Calculate angle at p2 in degrees."""
    v1 = subtract((p1.X, p1.Y, p1.Z), (p2.X, p2.Y, p2.Z))
    v2 = subtract((p3.X, p3.Y, p3.Z), (p2.X, p2.Y, p2.Z))
    v1 = normalize(v1)
    v2 = normalize(v2)
    d = max(-1, min(1, dot(v1, v2)))
    return math.acos(d) * 180 / math.pi

actual_angles = []
for sq in SQUARES:
    a, b, c, d = sq
    angle_at_a = calc_angle(dynamo_points[d], dynamo_points[a], dynamo_points[b])
    actual_angles.append(round(angle_at_a, 1))

# Calculate edge lengths for verification
edge_length_checks = []
for e in UNIQUE_EDGES[:6]:
    v1, v2 = e
    length = dynamo_points[v1].DistanceTo(dynamo_points[v2])
    edge_length_checks.append(round(length, 3))

# Determine state name
if rhombus_acute_angle > 85:
    state_name = "Vector Equilibrium"
elif abs(rhombus_acute_angle - 60) < 5:
    state_name = "Icosahedron"
elif rhombus_acute_angle < 5:
    state_name = "Octahedron"
else:
    state_name = "Transforming"

# =============================================================================
# OUTPUT
# =============================================================================

OUT = [
    dynamo_points,           # 0: All 12 vertices as Points
    edge_lines,              # 1: All 24 edges as Lines
    triangle_edges,          # 2: 24 triangle edges (STRUCTURAL)
    rhombus_edges,           # 3: Rhombus edges (subset, MOBILE)
    triangle_surfaces,       # 4: 8 triangular faces (STRUCTURAL)
    rhombus_surfaces,        # 5: 6 rhombus faces (MOBILE)
    state_name,              # 6: Current state name
    actual_angles,           # 7: Measured rhombus angles
    edge_length_checks,      # 8: Sample edge lengths (should all = edge_length)
]
