import tkinter as tk
import math
import random
from datetime import datetime

class WaxBlob:
    def __init__(self, x, y, radius, canvas_width, canvas_height):
        self.x = x
        self.y = y
        self.base_radius = radius
        self.vx = random.uniform(-0.3, 0.3)
        self.vy = random.uniform(-0.8, -0.2)
        self.canvas_width = canvas_width
        self.canvas_height = canvas_height
        self.density = radius * 0.1
        self.temperature = random.uniform(0.5, 1.0)
        self.age = 0

        # Shape deformation parameters
        self.oscillation_phase = random.uniform(0, 2 * math.pi)
        self.oscillation_speed = random.uniform(0.02, 0.08)
        self.deformation_strength = random.uniform(0.1, 0.3)

        # Control points for blob shape (relative to center)
        self.num_control_points = 6
        self.control_points = []
        self.control_phases = []
        self.control_speeds = []

        for i in range(self.num_control_points):
            angle = (2 * math.pi * i) / self.num_control_points
            self.control_points.append({
                'base_angle': angle,
                'radius_offset': random.uniform(0.8, 1.2),
                'tangent_length': random.uniform(0.3, 0.7)
            })
            self.control_phases.append(random.uniform(0, 2 * math.pi))
            self.control_speeds.append(random.uniform(0.01, 0.05))

        # Flow deformation based on velocity
        self.flow_memory = []
        self.max_flow_memory = 5

    @property
    def radius(self):
        return self.base_radius

    def update(self):
        self.age += 1

        # Heat rises, cool sinks
        heat_effect = (1.0 - self.y / self.canvas_height) * 0.3
        buoyancy = (self.temperature + heat_effect - 0.5) * 0.02

        # Gravity and buoyancy
        self.vy += 0.005 - buoyancy

        # Thermal currents
        thermal_x = math.sin(self.y * 0.01 + self.age * 0.02) * 0.1
        thermal_y = math.cos(self.x * 0.008 + self.age * 0.015) * 0.05

        self.vx += thermal_x
        self.vy += thermal_y

        # Damping
        self.vx *= 0.98
        self.vy *= 0.995

        # Store velocity for flow deformation
        velocity_magnitude = math.sqrt(self.vx**2 + self.vy**2)
        self.flow_memory.append(velocity_magnitude)
        if len(self.flow_memory) > self.max_flow_memory:
            self.flow_memory.pop(0)

        # Update position
        self.x += self.vx
        self.y += self.vy

        # Update shape oscillations
        self.oscillation_phase += self.oscillation_speed
        for i in range(len(self.control_phases)):
            self.control_phases[i] += self.control_speeds[i]

        # Boundary collision
        if self.x - self.base_radius <= 0:
            self.x = self.base_radius
            self.vx = abs(self.vx) * 0.3 + random.uniform(0, 0.2)
        elif self.x + self.base_radius >= self.canvas_width:
            self.x = self.canvas_width - self.base_radius
            self.vx = -abs(self.vx) * 0.3 - random.uniform(0, 0.2)

        if self.y - self.base_radius <= 0:
            self.y = self.base_radius
            self.vy = abs(self.vy) * 0.4
            self.temperature = max(0.2, self.temperature - 0.1)
        elif self.y + self.base_radius >= self.canvas_height:
            self.y = self.canvas_height - self.base_radius
            self.vy = -abs(self.vy) * 0.4
            self.temperature = min(1.0, self.temperature + 0.2)

    def get_bezier_control_points(self):
        """Generate control points for bezier curves that form the blob shape"""
        avg_velocity = sum(self.flow_memory) / max(len(self.flow_memory), 1)
        velocity_angle = math.atan2(self.vy, self.vx)

        control_points = []

        for i in range(self.num_control_points):
            cp = self.control_points[i]

            # Base position
            base_angle = cp['base_angle']

            # Oscillation deformation
            oscillation = math.sin(self.oscillation_phase + i * 0.8) * self.deformation_strength

            # Individual point oscillation
            point_oscillation = math.sin(self.control_phases[i]) * 0.2

            # Flow-based deformation
            flow_factor = avg_velocity * 0.8
            angle_diff = abs(base_angle - velocity_angle)
            angle_diff = min(angle_diff, 2 * math.pi - angle_diff)

            if angle_diff < math.pi / 2:
                flow_deformation = flow_factor * (1 - angle_diff / (math.pi / 2)) * 0.4
            else:
                flow_deformation = -flow_factor * 0.15

            # Thermal deformation
            thermal_deformation = self.temperature * 0.15 * math.sin(self.age * 0.03 + i * 1.2)

            # Combined radius for this control point
            radius_multiplier = (cp['radius_offset'] + oscillation + point_oscillation +
                               flow_deformation + thermal_deformation)
            radius_multiplier = max(0.4, min(1.8, radius_multiplier))

            point_radius = self.base_radius * radius_multiplier

            # Main control point
            x = self.x + math.cos(base_angle) * point_radius
            y = self.y + math.sin(base_angle) * point_radius

            # Tangent control points for bezier curves
            tangent_length = self.base_radius * cp['tangent_length'] * radius_multiplier
            tangent_angle1 = base_angle - math.pi / 2
            tangent_angle2 = base_angle + math.pi / 2

            # Add some flow-based tangent adjustment
            flow_tangent_adjust = avg_velocity * 0.3
            tangent_length *= (1 + flow_tangent_adjust)

            tx1 = x + math.cos(tangent_angle1) * tangent_length
            ty1 = y + math.sin(tangent_angle1) * tangent_length
            tx2 = x + math.cos(tangent_angle2) * tangent_length
            ty2 = y + math.sin(tangent_angle2) * tangent_length

            control_points.append({
                'point': (x, y),
                'tangent1': (tx1, ty1),
                'tangent2': (tx2, ty2)
            })

        return control_points

    def distance_to(self, other):
        return math.sqrt((self.x - other.x)**2 + (self.y - other.y)**2)

    def can_merge_with(self, other):
        distance = self.distance_to(other)
        merge_distance = (self.base_radius + other.base_radius) * 0.8
        return distance < merge_distance

    def merge_with(self, other):
        # Conservation of mass
        total_area = math.pi * (self.base_radius**2 + other.base_radius**2)
        new_radius = math.sqrt(total_area / math.pi)

        # Weighted average
        total_mass = self.base_radius**2 + other.base_radius**2
        new_x = (self.x * self.base_radius**2 + other.x * other.base_radius**2) / total_mass
        new_y = (self.y * self.base_radius**2 + other.y * other.base_radius**2) / total_mass
        new_vx = (self.vx * self.base_radius**2 + other.vx * other.base_radius**2) / total_mass
        new_vy = (self.vy * self.base_radius**2 + other.vy * other.base_radius**2) / total_mass
        new_temp = (self.temperature + other.temperature) / 2

        merged = WaxBlob(new_x, new_y, new_radius, self.canvas_width, self.canvas_height)
        merged.vx = new_vx
        merged.vy = new_vy
        merged.temperature = new_temp

        # Blend shape parameters
        merged.oscillation_phase = (self.oscillation_phase + other.oscillation_phase) / 2
        merged.oscillation_speed = (self.oscillation_speed + other.oscillation_speed) / 2
        merged.deformation_strength = (self.deformation_strength + other.deformation_strength) / 2

        return merged

    def should_split(self):
        if self.base_radius > 40:
            return random.random() < 0.002
        return False

    def split(self):
        split_ratio = random.uniform(0.3, 0.7)
        area1 = math.pi * self.base_radius**2 * split_ratio
        area2 = math.pi * self.base_radius**2 * (1 - split_ratio)

        radius1 = math.sqrt(area1 / math.pi)
        radius2 = math.sqrt(area2 / math.pi)

        angle = random.uniform(0, 2 * math.pi)
        offset = (radius1 + radius2) * 0.6

        blob1 = WaxBlob(
            self.x + math.cos(angle) * offset,
            self.y + math.sin(angle) * offset,
            radius1, self.canvas_width, self.canvas_height
        )
        blob2 = WaxBlob(
            self.x - math.cos(angle) * offset,
            self.y - math.sin(angle) * offset,
            radius2, self.canvas_width, self.canvas_height
        )

        blob1.vx = self.vx + random.uniform(-0.5, 0.5)
        blob1.vy = self.vy + random.uniform(-0.5, 0.5)
        blob2.vx = self.vx + random.uniform(-0.5, 0.5)
        blob2.vy = self.vy + random.uniform(-0.5, 0.5)

        blob1.temperature = self.temperature + random.uniform(-0.1, 0.1)
        blob2.temperature = self.temperature + random.uniform(-0.1, 0.1)

        # Vary shape parameters
        blob1.oscillation_speed = self.oscillation_speed + random.uniform(-0.02, 0.02)
        blob2.oscillation_speed = self.oscillation_speed + random.uniform(-0.02, 0.02)
        blob1.deformation_strength = self.deformation_strength + random.uniform(-0.1, 0.1)
        blob2.deformation_strength = self.deformation_strength + random.uniform(-0.1, 0.1)

        return [blob1, blob2]

class LavaLampSimulator:
    def __init__(self, root):
        self.root = root
        self.root.title("Lava Lamp Simulator - Bezier Blobs")
        self.root.geometry("400x600")
        self.root.configure(bg='black')

        # Create canvas
        self.canvas = tk.Canvas(root, width=380, height=580, bg='#1a0d0d', highlightthickness=0)
        self.canvas.pack(pady=10)

        # Initialize blobs
        self.blobs = []
        self.create_initial_blobs()

        # Start animation
        self.animate()

    def create_initial_blobs(self):
        for _ in range(6):
            x = random.uniform(50, 330)
            y = random.uniform(100, 500)
            radius = random.uniform(20, 35)
            blob = WaxBlob(x, y, radius, 380, 580)
            self.blobs.append(blob)

    def get_blob_color(self, blob):
        temp = blob.temperature
        base_red = int(255 * (0.7 + temp * 0.3))
        base_green = int(100 * temp)
        base_blue = int(50 * temp)

        height_factor = 1.0 - (blob.y / 580)
        red = min(255, int(base_red * (0.8 + height_factor * 0.2)))
        green = min(255, int(base_green * (0.8 + height_factor * 0.2)))
        blue = min(255, int(base_blue * (0.8 + height_factor * 0.2)))

        return f"#{red:02x}{green:02x}{blue:02x}"

    def bezier_point(self, t, p0, p1, p2, p3):
        """Calculate a point on a cubic bezier curve"""
        u = 1 - t
        return (
            u**3 * p0[0] + 3 * u**2 * t * p1[0] + 3 * u * t**2 * p2[0] + t**3 * p3[0],
            u**3 * p0[1] + 3 * u**2 * t * p1[1] + 3 * u * t**2 * p2[1] + t**3 * p3[1]
        )

    def create_bezier_blob_outline(self, blob, resolution=20):
        """Create a smooth blob outline using bezier curves"""
        control_points = blob.get_bezier_control_points()
        outline_points = []

        num_segments = len(control_points)

        for i in range(num_segments):
            # Current and next control point
            curr = control_points[i]
            next_i = (i + 1) % num_segments
            next_cp = control_points[next_i]

            # Define bezier curve from current point to next point
            p0 = curr['point']
            p1 = curr['tangent2']  # Outgoing tangent from current point
            p2 = next_cp['tangent1']  # Incoming tangent to next point
            p3 = next_cp['point']

            # Generate points along the bezier curve
            for j in range(resolution):
                if i == num_segments - 1 and j == resolution - 1:
                    # Skip the last point to avoid duplication
                    break

                t = j / resolution
                point = self.bezier_point(t, p0, p1, p2, p3)
                outline_points.extend([point[0], point[1]])

        return outline_points

    def draw_blob(self, blob):
        color = self.get_blob_color(blob)

        try:
            # Get bezier outline points
            outline_points = self.create_bezier_blob_outline(blob)

            if len(outline_points) >= 6:
                # Draw the main blob
                self.canvas.create_polygon(outline_points, fill=color, outline='', smooth=True)

                # Create highlight with scaled bezier shape
                highlight_points = []
                highlight_scale = 0.4
                highlight_center_x = blob.x - blob.base_radius * 0.3
                highlight_center_y = blob.y - blob.base_radius * 0.3

                for i in range(0, len(outline_points), 2):
                    orig_x = outline_points[i]
                    orig_y = outline_points[i + 1]

                    # Vector from highlight center to point
                    vec_x = orig_x - highlight_center_x
                    vec_y = orig_y - highlight_center_y

                    # Scale toward highlight center
                    new_x = highlight_center_x + vec_x * highlight_scale
                    new_y = highlight_center_y + vec_y * highlight_scale

                    highlight_points.extend([new_x, new_y])

                # Lighter color for highlight
                r = min(255, int(color[1:3], 16) + 40)
                g = min(255, int(color[3:5], 16) + 20)
                b = min(255, int(color[5:7], 16) + 10)
                highlight_color = f"#{r:02x}{g:02x}{b:02x}"

                if len(highlight_points) >= 6:
                    self.canvas.create_polygon(highlight_points, fill=highlight_color,
                                             outline='', smooth=True)
            else:
                self.draw_fallback_circle(blob, color)

        except (tk.TclError, ValueError):
            # Fallback to circle if bezier fails
            self.draw_fallback_circle(blob, color)

    def draw_fallback_circle(self, blob, color):
        """Fallback method to draw a simple circle"""
        x1 = blob.x - blob.base_radius
        y1 = blob.y - blob.base_radius
        x2 = blob.x + blob.base_radius
        y2 = blob.y + blob.base_radius
        self.canvas.create_oval(x1, y1, x2, y2, fill=color, outline='')

    def update_physics(self):
        # Update all blobs
        for blob in self.blobs:
            blob.update()

        # Handle merging
        merged_blobs = []
        used_indices = set()

        for i, blob1 in enumerate(self.blobs):
            if i in used_indices:
                continue

            merged = False
            for j, blob2 in enumerate(self.blobs[i+1:], i+1):
                if j in used_indices:
                    continue

                if blob1.can_merge_with(blob2):
                    new_blob = blob1.merge_with(blob2)
                    merged_blobs.append(new_blob)
                    used_indices.add(i)
                    used_indices.add(j)
                    merged = True
                    break

            if not merged:
                merged_blobs.append(blob1)

        self.blobs = merged_blobs

        # Handle splitting
        new_blobs = []
        for blob in self.blobs:
            if blob.should_split():
                split_blobs = blob.split()
                new_blobs.extend(split_blobs)
            else:
                new_blobs.append(blob)

        self.blobs = new_blobs

        # Maintain minimum number of blobs
        if len(self.blobs) < 3:
            x = random.uniform(50, 330)
            y = random.uniform(400, 550)
            radius = random.uniform(15, 25)
            new_blob = WaxBlob(x, y, radius, 380, 580)
            new_blob.temperature = 0.8
            self.blobs.append(new_blob)

    def animate(self):
        # Clear canvas
        self.canvas.delete("all")

        # Update physics
        self.update_physics()

        # Draw all blobs
        for blob in self.blobs:
            self.draw_blob(blob)

        # Schedule next frame
        self.root.after(50, self.animate)

# Add the missing method to WaxBlob class
def get_bezier_control_points(self):
    """Generate control points for bezier curves that form the blob shape"""
    avg_velocity = sum(self.flow_memory) / max(len(self.flow_memory), 1)
    velocity_angle = math.atan2(self.vy, self.vx)

    control_points = []

    for i in range(self.num_control_points):
        cp = self.control_points[i]

        # Base position
        base_angle = cp['base_angle']

        # Oscillation deformation
        oscillation = math.sin(self.oscillation_phase + i * 0.8) * self.deformation_strength

        # Individual point oscillation
        point_oscillation = math.sin(self.control_phases[i]) * 0.2

        # Flow-based deformation
        flow_factor = avg_velocity * 0.8
        angle_diff = abs(base_angle - velocity_angle)
        angle_diff = min(angle_diff, 2 * math.pi - angle_diff)

        if angle_diff < math.pi / 2:
            flow_deformation = flow_factor * (1 - angle_diff / (math.pi / 2)) * 0.4
        else:
            flow_deformation = -flow_factor * 0.15

        # Thermal deformation
        thermal_deformation = self.temperature * 0.15 * math.sin(self.age * 0.03 + i * 1.2)

        # Combined radius for this control point
        radius_multiplier = (cp['radius_offset'] + oscillation + point_oscillation +
                           flow_deformation + thermal_deformation)
        radius_multiplier = max(0.4, min(1.8, radius_multiplier))

        point_radius = self.base_radius * radius_multiplier

        # Main control point position
        x = self.x + math.cos(base_angle) * point_radius
        y = self.y + math.sin(base_angle) * point_radius

        # Calculate tangent control points for smooth bezier curves
        tangent_length = self.base_radius * cp['tangent_length'] * radius_multiplier * 0.5

        # Tangent direction (perpendicular to radius, with some variation)
        tangent_base_angle = base_angle + math.pi / 2
        tangent_variation = math.sin(self.control_phases[i] * 2) * 0.3
        tangent_angle = tangent_base_angle + tangent_variation

        # Flow influence on tangents
        flow_influence = avg_velocity * 0.4
        flow_angle_influence = math.sin(base_angle - velocity_angle) * flow_influence
        tangent_angle += flow_angle_influence

        # Tangent control points (before and after the main point)
        tx1 = x - math.cos(tangent_angle) * tangent_length
        ty1 = y - math.sin(tangent_angle) * tangent_length
        tx2 = x + math.cos(tangent_angle) * tangent_length
        ty2 = y + math.sin(tangent_angle) * tangent_length

        control_points.append({
            'point': (x, y),
            'tangent1': (tx1, ty1),
            'tangent2': (tx2, ty2)
        })

    return control_points

# Attach the method to the WaxBlob class
WaxBlob.get_bezier_control_points = get_bezier_control_points

if __name__ == "__main__":
    root = tk.Tk()
    app = LavaLampSimulator(root)
    root.mainloop()