"""
Title: Heatmap Intensity Template

Description:
A reusable Manim template for visualizing intensity, metrics, or activity using dynamic heat maps, meters, and data streams.
All data, labels, and styles are configurable via dynamic_data. Animation duration is precisely controlled.

Expected dynamic_data format:
{
    "title": str,  # Scene title
    "heat_data": List[{"pos": [x, y, z], "intensity": float}],
    "meters": List[{"value": float, "label": str}],
    "style": Dict[str, str],  # Optional style overrides
    "title_font_size": int,
    "meter_font_size": int,
    ...
}

Tags:
- heatmap
- intensity
- metric
- infographic
- animation
- manim
- template
- render_image
"""

from manim import *
import numpy as np
from typing import Dict, List, Any


class HeatmapIntensityTemplate(MovingCameraScene):
    def __init__(self, dynamic_data: Dict = {}, duration: int = 10, aspect_ratio: str = "16:9", **kwargs):
        super().__init__(**kwargs)
        self.dynamic_data = dynamic_data
        self.duration = duration
        self.aspect_ratio = aspect_ratio
        # Default style
        self.style = {
            "primary": "#00BCD4",     # Cyan
            "secondary": "#FF4081",    # Pink
            "accent": "#FFEB3B",       # Yellow
            "neutral": "#FAFAFA",      # White
            "dark": "#1A1A1A",         # Almost Black
            "heat_high": "#FF0000",    # Red
            "heat_med": "#FF8C00",     # Orange
            "heat_low": "#FFD700",     # Gold
            "glow": "#80DEEA"         # Light Cyan
        }
        # Allow style override
        if "style" in self.dynamic_data:
            self.style.update(self.dynamic_data["style"])

    def create_heat_map(self, data_points: List[Dict]) -> VGroup:
        """Creates an animated heat map with glowing effects."""
        heat_map = VGroup()

        # Background grid
        grid = VGroup()
        if self.aspect_ratio == "9:16":
            x_range, y_range = range(-2, 3), range(-4, 5)
        elif self.aspect_ratio == "1:1":
            x_range, y_range = range(-3, 4), range(-3, 4)
        else:  # "16:9"
            x_range, y_range = range(-4, 5), range(-2, 3)

        for x in x_range:
            for y in range(-2, 3):
                cell = Square(
                    side_length=1,
                    stroke_color=self.style["primary"],
                    stroke_width=1,
                    stroke_opacity=0.3
                ).move_to([x, y, 0])
                grid.add(cell)

        # Heat points with intensity
        heat_points = VGroup()
        for point in data_points:
            # Main heat circle
            intensity = point["intensity"]
            color = self.style["heat_high"] if intensity > 0.7 else \
                self.style["heat_med"] if intensity > 0.4 else \
                self.style["heat_low"]

            circle = Circle(
                radius=intensity * 0.8,
                fill_color=color,
                fill_opacity=0.6,
                stroke_width=0
            ).move_to(point["pos"])

            # Pulsing effect
            pulses = VGroup()
            for i in range(3):
                pulse = circle.copy()
                pulse.scale(1 + i * 0.3)
                pulse.set_opacity(0.3 - i * 0.1)
                pulses.add(pulse)

            heat_points.add(VGroup(circle, pulses))

        heat_map.add(grid, heat_points)
        return heat_map

    def create_data_stream(self, start: np.ndarray, end: np.ndarray) -> VGroup:
        """Creates flowing data stream effect."""
        stream = VGroup()

        # Main path
        path = CubicBezier(
            start,
            start + UP + RIGHT,
            end + DOWN + LEFT,
            end
        )

        # Particle system
        particles = VGroup()
        for i in range(10):
            alpha = i / 10
            particle = Dot(
                radius=0.05,
                color=self.style["accent"],
                fill_opacity=0.8
            ).move_to(path.point_from_proportion(alpha))
            particles.add(particle)

        # Path glow
        glow = path.copy().set_stroke(
            self.style["glow"],
            width=8,
            opacity=0.3
        )

        stream.add(glow, path, particles)
        return stream

    def create_intensity_meter(self, value: float, label: str, font_size: int = 24) -> VGroup:
        """Creates an animated intensity meter."""
        meter = VGroup()

        if self.aspect_ratio == "9:16":
            meter_radius = 0.7
        elif self.aspect_ratio == "1:1":
            meter_radius = 0.85
        else:  # "16:9"
            meter_radius = 1.0

        # Background arc
        bg_arc = Arc(
            radius=meter_radius,
            angle=PI,
            stroke_color=self.style["neutral"],
            stroke_width=4,
            stroke_opacity=0.3
        )

        # Value arc
        value_arc = Arc(
            radius=meter_radius,
            angle=PI * value,
            stroke_color=self.style["accent"],
            stroke_width=6
        )

        # Label
        text = Text(
            label,
            font="Arial Bold",
            font_size=font_size,
            color=self.style["neutral"]
        ).next_to(bg_arc, DOWN, buff=0.3)

        # Value display
        value_text = Text(
            f"{int(value * 100)}%",
            font="Arial Black",
            font_size=font_size + 8,
            color=self.style["accent"]
        ).move_to(bg_arc)

        meter.add(bg_arc, value_arc, text, value_text)
        return meter

    def construct(self):
        """Constructs the scene with dynamic battle intensity visualization."""
        # --- Aspect Ratio Config ---
        if self.aspect_ratio == "9:16":
            config.pixel_width, config.pixel_height = 1080, 1920
            config.frame_width, config.frame_height = 9, 16
        elif self.aspect_ratio == "1:1":
            config.pixel_width, config.pixel_height = 1080, 1080
            config.frame_width, config.frame_height = 8, 8
        else:  # "16:9"
            config.pixel_width, config.pixel_height = 1920, 1080
            config.frame_width, config.frame_height = 16, 9

        # --- Extract dynamic data with defaults ---
        title = self.dynamic_data.get("title", "Heatmap Intensity Analysis")
        heat_data = self.dynamic_data.get("heat_data", [
            {"pos": [-2, 1, 0], "intensity": 0.9},
            {"pos": [1, -1, 0], "intensity": 0.7},
            {"pos": [-1, -1, 0], "intensity": 0.5},
            {"pos": [2, 1, 0], "intensity": 0.8}
        ])
        meters_data = self.dynamic_data.get("meters", [
            {"value": 0.85, "label": "Metric 1"},
            {"value": 0.75, "label": "Metric 2"},
            {"value": 0.92, "label": "Metric 3"}
        ])
        title_font_size = self.dynamic_data.get("title_font_size", 48)
        meter_font_size = self.dynamic_data.get("meter_font_size", 24)

        # --- Adjustments for Aspect Ratio ---
        if self.aspect_ratio == "9:16":
            title_font_size *= 0.7
            meter_font_size *= 0.7
        elif self.aspect_ratio == "1:1":
            title_font_size *= 0.85
            meter_font_size *= 0.85

        # --- Create and layout elements ---
        title_mob = Text(
            title,
            font="Arial Black",
            font_size=title_font_size,
            color=self.style["primary"]
        )
        heat_map = self.create_heat_map(heat_data)
        meters = VGroup(*[
            self.create_intensity_meter(
                m["value"], m["label"], meter_font_size)
            for m in meters_data
        ])

        if self.aspect_ratio == "9:16":
            title_mob.to_edge(UP, buff=0.5)
            meters.arrange(DOWN, buff=0.5).to_edge(LEFT, buff=0.5)
            heat_map.to_edge(RIGHT, buff=0.5)
        elif self.aspect_ratio == "1:1":
            title_mob.to_edge(UP, buff=0.8)
            meters.arrange(RIGHT, buff=0.5).to_edge(DOWN, buff=1)
        else:  # "16:9"
            title_mob.to_edge(UP, buff=0.8)
            meters.arrange(RIGHT, buff=1.5).to_edge(DOWN, buff=1)

        # Data streams: match up to min(len(heat_points), len(meters))
        n_streams = min(len(heat_map[1]), len(meters))
        streams = VGroup(*[
            self.create_data_stream(
                heat_map[1][i][0].get_center(),
                meters[i].get_top()
            )
            for i in range(n_streams)
        ])
        self.camera.frame.save_state()

        # --- Animation step weights ---
        steps = [
            {"name": "title", "weight": 1},
            {"name": "grid", "weight": 1},
            {"name": "heat_points", "weight": len(heat_map[1])},
            {"name": "meters", "weight": len(meters)},
            {"name": "streams", "weight": n_streams},
            {"name": "final_emphasis", "weight": 1},
            {"name": "final_wait", "weight": 0.5}
        ]
        total_weight = sum(step["weight"] for step in steps)
        for step in steps:
            step["duration"] = (step["weight"] / total_weight) * self.duration

        # --- Animation sequence ---
        self.play(Write(title_mob), run_time=steps[0]["duration"])
        self.play(Create(heat_map[0]), run_time=steps[1]["duration"])
        # Heat points: split duration evenly among all points
        heat_point_time = steps[2]["duration"] / max(1, len(heat_map[1]))
        for point in heat_map[1]:
            self.play(
                FadeIn(point[0], scale=1.2),
                *[FadeIn(pulse, scale=1.1) for pulse in point[1]],
                run_time=heat_point_time
            )
        # Meters: split duration evenly among all meters, and split further for arc and label/value
        meter_time = steps[3]["duration"] / max(1, len(meters))
        for meter in meters:
            self.play(
                self.camera.frame.animate.scale(0.9).move_to(meter),
                Create(meter[0]),
                Create(meter[1]),
                run_time=meter_time * 0.5
            )
            self.play(
                Write(meter[2]),
                FadeIn(meter[3], scale=1.1),
                run_time=meter_time * 0.5
            )
        # Streams: split duration among all streams, and split further for path and particles
        # Camera transition uses 20% of streams step, rest is for streams
        self.play(
            self.camera.frame.animate.scale(1.2).move_to(ORIGIN),
            run_time=steps[4]["duration"] * 0.2
        )
        stream_time = (steps[4]["duration"] * 0.8) / max(1, n_streams)
        for stream in streams:
            self.play(
                Create(stream[0]),
                Create(stream[1]),
                run_time=stream_time * 0.5
            )
            self.play(
                LaggedStartMap(FadeIn, stream[2], lag_ratio=0.1),
                run_time=stream_time * 0.5
            )
        self.play(
            self.camera.frame.animate.scale(1.1),
            *[
                Flash(
                    point=point[0].get_center(),
                    color=point[0].get_color(),
                    line_length=0.5,
                    flash_radius=0.4,
                    num_lines=8
                )
                for point in heat_map[1]
            ],
            run_time=steps[5]["duration"]
        )

        def heat_updater(mob, dt):
            mob.time = getattr(mob, "time", 0) + dt
            scale = 1 + 0.1 * np.sin(mob.time * 2)
            mob.set_opacity(0.6 + 0.2 * np.sin(mob.time * 2))
            mob.scale_to_fit_width(mob.initial_width * scale)
        for point in heat_map[1]:
            point[0].initial_width = point[0].get_width()
            point[0].add_updater(heat_updater)
        self.wait(steps[6]["duration"])
        for point in heat_map[1]:
            point[0].clear_updaters() // এমন একটা ট্যামপ্লেট তৈরি করে দাও ভিন্ন ক্যাটা গরির এবং ভিন্ন ডিজািনের