from manim import *
import numpy as np
import math

def safe_text(text, max_width=10, font_size=28, color="#E8E6E1"):
    t = Text(text, font_size=font_size, color=color)
    if t.width > max_width:
        t.scale(max_width / t.width)
    return t

class GeneratedScene(Scene):
    def construct(self):
        self.camera.background_color = "#0E1116"

        CORAL = "#FF6B5B"
        CYAN  = "#4ECDC4"
        TEXT_C = "#E8E6E1"

        A0 = 1.6
        left_center = np.array([-3.2, 0, 0])
        x_start = 0.6
        x_end   = 6.4
        period_width = 2.2

        theta_tracker   = ValueTracker(0)
        nh_tracker      = ValueTracker(1)
        periods_tracker = ValueTracker(1)
        trace_op_tracker = ValueTracker(1.0)

        # ---- math helpers ----
        def get_endpoint():
            theta = theta_tracker.get_value()
            nh = int(nh_tracker.get_value())
            pos = left_center.copy()
            for k in range(nh):
                n = 2 * k + 1
                r = A0 / n
                ang = n * theta
                pos = pos + r * np.array([np.cos(ang), np.sin(ang), 0])
            return pos

        def get_wave_y(t, nh):
            return sum((A0 / (2 * k + 1)) * np.sin((2 * k + 1) * t) for k in range(nh))

        def get_draw_x():
            theta = theta_tracker.get_value()
            periods = periods_tracker.get_value()
            max_theta = periods * 2 * np.pi
            draw_theta = min(theta, max_theta)
            return x_start + (draw_theta / (2 * np.pi)) * period_width

        def make_static_trace(nh, periods, color, opacity):
            max_theta = periods * 2 * np.pi
            n_samples = 300
            pts = []
            for i in range(n_samples + 1):
                t = max_theta * i / n_samples
                x = x_start + (t / (2 * np.pi)) * period_width
                y = get_wave_y(t, nh)
                pts.append(np.array([x, y, 0]))
            line = VMobject()
            line.set_points_smoothly(np.array(pts))
            line.set_stroke(color, 2, opacity=opacity)
            return line

        # ---- always-redraw epicycles ----
        def make_epicycles():
            theta = theta_tracker.get_value()
            nh = int(nh_tracker.get_value())
            group = VGroup()
            pos = left_center.copy()
            for k in range(nh):
                n = 2 * k + 1
                r = A0 / n
                ang = n * theta
                op = max(0.35, 1.0 - 0.22 * k)

                circle = Circle(radius=r, color=CORAL, stroke_width=1.5)
                circle.move_to(pos)
                circle.set_stroke(opacity=op * 0.5)
                group.add(circle)

                new_pos = pos + r * np.array([np.cos(ang), np.sin(ang), 0])
                vec = Line(pos, new_pos, color=CORAL, stroke_width=2.5)
                vec.set_stroke(opacity=op)
                group.add(vec)
                pos = new_pos

            tip = Dot(pos, radius=0.05, color=CYAN)
            group.add(tip)
            return group

        epicycles = always_redraw(make_epicycles)

        # ---- guide line ----
        def make_guide():
            ep = get_endpoint()
            dx = get_draw_x()
            line = DashedLine(ep, np.array([dx, ep[1], 0]),
                              color=TEXT_C, stroke_width=1)
            line.set_stroke(opacity=0.35)
            return line

        guide = always_redraw(make_guide)

        # ---- waveform trace ----
        def make_trace():
            theta = theta_tracker.get_value()
            nh = int(nh_tracker.get_value())
            periods = periods_tracker.get_value()
            max_theta = periods * 2 * np.pi
            draw_theta = min(theta, max_theta)

            if draw_theta < 0.01:
                return VMobject()

            n_samples = max(10, int(draw_theta / (2 * np.pi) * 250))
            pts = []
            for i in range(n_samples + 1):
                t = draw_theta * i / n_samples
                x = x_start + (t / (2 * np.pi)) * period_width
                y = get_wave_y(t, nh)
                pts.append(np.array([x, y, 0]))

            if len(pts) < 2:
                return VMobject()
            line = VMobject()
            line.set_points_smoothly(np.array(pts))
            line.set_stroke(CYAN, 2.5)
            line.set_stroke(opacity=trace_op_tracker.get_value())
            return line

        trace = always_redraw(make_trace)

        # ---- static frame elements ----
        zero_line = DashedLine(
            np.array([x_start, 0, 0]), np.array([x_end, 0, 0]),
            color=TEXT_C, stroke_width=1)
        zero_line.set_stroke(opacity=0.15)

        divider = DashedLine(
            np.array([-0.8, -2.5, 0]), np.array([-0.8, 2.5, 0]),
            color=TEXT_C, stroke_width=1)
        divider.set_stroke(opacity=0.1)

        tick_marks = VGroup()
        for i in range(4):
            tx = x_start + i * period_width
            if tx <= x_end + 0.1:
                tk = Line(
                    np.array([tx, -0.06, 0]), np.array([tx, 0.06, 0]),
                    color=TEXT_C, stroke_width=1)
                tk.set_stroke(opacity=0.3)
                tick_marks.add(tk)

        self.add(zero_line, divider, tick_marks)

        # ================================================================
        # SCENE 1 — Fundamental vector  (6 s)
        # ================================================================
        self.add(epicycles, guide, trace)

        label_1w = safe_text("1×ω", max_width=2, font_size=28, color=CORAL)
        label_1w.move_to(np.array([-3.2, 2.3, 0]))

        self.play(FadeIn(label_1w), run_time=0.8)
        self.wait(0.2)

        self.play(theta_tracker.animate.set_value(2 * np.pi),
                  run_time=4, rate_func=linear)

        subtitle = safe_text("square wave = sum of odd sines",
                             max_width=8, font_size=24, color=TEXT_C)
        subtitle.to_edge(DOWN, buff=0.5)
        self.play(FadeIn(subtitle), run_time=0.8)
        self.wait(0.5)

        # ================================================================
        # SCENE 2 — 3rd harmonic  (7 s)
        # ================================================================
        ghost1 = make_static_trace(1, 1, CYAN, 0.2)
        self.add(ghost1)

        self.play(FadeOut(subtitle), FadeOut(label_1w), run_time=0.5)

        theta_tracker.set_value(0)
        nh_tracker.set_value(2)

        f2 = MathTex(r"+\,\frac{1}{3}\sin(3\omega)",
                     font_size=30, color=TEXT_C)
        f2.move_to(np.array([1.8, 2.8, 0]))

        self.play(FadeIn(f2), run_time=0.6)
        self.wait(0.2)

        self.play(theta_tracker.animate.set_value(2 * np.pi),
                  run_time=4, rate_func=linear)
        self.wait(0.8)

        # ================================================================
        # SCENE 3 — 5th harmonic  (7 s)
        # ================================================================
        ghost2 = make_static_trace(2, 1, CYAN, 0.2)
        self.add(ghost2)
        self.play(FadeOut(ghost1), run_time=0.3)

        theta_tracker.set_value(0)
        nh_tracker.set_value(3)

        f3 = MathTex(r"+\,\frac{1}{5}\sin(5\omega)",
                     font_size=30, color=TEXT_C)
        f3.next_to(f2, DOWN, buff=0.2)

        self.play(FadeIn(f3), run_time=0.6)
        self.wait(0.2)

        self.play(theta_tracker.animate.set_value(2 * np.pi),
                  run_time=4, rate_func=linear)
        self.wait(0.8)

        # ================================================================
        # SCENE 4 — 7th harmonic  (7 s)
        # ================================================================
        ghost3 = make_static_trace(3, 1, CYAN, 0.2)
        self.add(ghost3)
        self.play(FadeOut(ghost2), run_time=0.3)

        theta_tracker.set_value(0)
        nh_tracker.set_value(4)

        f4 = MathTex(r"+\,\frac{1}{7}\sin(7\omega)",
                     font_size=30, color=TEXT_C)
        f4.next_to(f3, DOWN, buff=0.2)

        self.play(FadeIn(f4), run_time=0.6)
        self.wait(0.2)

        self.play(theta_tracker.animate.set_value(2 * np.pi),
                  run_time=4, rate_func=linear)
        self.wait(0.8)

        # ================================================================
        # SCENE 5 — Gibbs overshoot  (6 s)
        # ================================================================
        self.play(FadeOut(ghost3), run_time=0.3)
        periods_tracker.set_value(2)

        # compute overshoot numerically
        t_arr = np.linspace(0.01, np.pi, 5000)
        y_arr = np.array([
            sum(np.sin((2 * k + 1) * t) / (2 * k + 1) for k in range(4))
            for t in t_arr
        ])
        peak_idx = int(np.argmax(y_arr))
        peak_y_val = y_arr[peak_idx] * A0
        limit_y_val = (np.pi / 4) * A0

        highlight_box = Rectangle(
            width=1.4, height=1.6,
            color=CYAN, stroke_width=1.5, fill_opacity=0.06)
        highlight_box.move_to(np.array([x_start + 0.5,
                                        limit_y_val + 0.15, 0]))

        ref_line = DashedLine(
            np.array([x_start - 0.1, limit_y_val, 0]),
            np.array([x_start + 2.8, limit_y_val, 0]),
            color=TEXT_C, stroke_width=1)
        ref_line.set_stroke(opacity=0.4)

        overshoot_line = DashedLine(
            np.array([x_start - 0.1, peak_y_val, 0]),
            np.array([x_start + 2.8, peak_y_val, 0]),
            color=CORAL, stroke_width=1)
        overshoot_line.set_stroke(opacity=0.5)

        gibbs_label = safe_text("Gibbs overshoot",
                                max_width=3, font_size=22, color=CYAN)
        gibbs_label.next_to(highlight_box, UP, buff=0.15)

        pct_label = safe_text("≈ 9% overshoot",
                              max_width=3, font_size=20, color=TEXT_C)
        pct_label.next_to(gibbs_label, UP, buff=0.1)

        self.play(FadeIn(highlight_box), run_time=0.8)
        self.play(FadeIn(ref_line), FadeIn(overshoot_line), run_time=0.6)
        self.play(FadeIn(gibbs_label), FadeIn(pct_label), run_time=0.6)

        self.play(theta_tracker.animate.set_value(4 * np.pi),
                  run_time=3.0, rate_func=linear)
        self.wait(0.5)

        self.play(
            FadeOut(highlight_box), FadeOut(ref_line),
            FadeOut(overshoot_line), FadeOut(gibbs_label),
            FadeOut(pct_label),
            run_time=0.5)

        # ================================================================
        # SCENE 6 — Full composition  (8 s)
        # ================================================================
        # fade trace out, reset, fade grid + trace in, draw 2.5 periods
        self.play(trace_op_tracker.animate.set_value(0), run_time=0.4)
        theta_tracker.set_value(0)
        periods_tracker.set_value(2.5)

        grid = VGroup()
        for i in range(-3, 4):
            h = Line(np.array([-6.6, i, 0]), np.array([6.6, i, 0]),
                     color=TEXT_C, stroke_width=0.5)
            h.set_stroke(opacity=0.06)
            grid.add(h)
        for i in range(-6, 7):
            v = Line(np.array([i, -3.5, 0]), np.array([i, 3.5, 0]),
                     color=TEXT_C, stroke_width=0.5)
            v.set_stroke(opacity=0.06)
            grid.add(v)

        self.play(FadeIn(grid), trace_op_tracker.animate.set_value(1.0),
                  run_time=1.0)

        self.play(theta_tracker.animate.set_value(5 * np.pi),
                  run_time=5, rate_func=linear)
        self.wait(1.5)

        # ================================================================
        # SCENE 7 — Final formula & hold  (7 s)
        # ================================================================
        # ease rotation to a halt at 6π (vectors aligned rightward)
        self.play(theta_tracker.animate.set_value(6 * np.pi),
                  run_time=1.5, rate_func=smooth)

        self.play(FadeOut(f2), FadeOut(f3), FadeOut(f4), run_time=0.8)

        formula = MathTex(
            r"f(t) = ", r"\frac{4}{\pi}",
            r"\sum_{k=1}^{\infty}",
            r"\frac{\sin((2k-1)\,t)}{2k-1}",
            font_size=34
        )
        formula[0].set_color(TEXT_C)
        formula[1].set_color(CORAL)
        formula[2].set_color(TEXT_C)
        formula[3].set_color(TEXT_C)

        if formula.width > 11:
            formula.scale(11 / formula.width)
        formula.to_edge(DOWN, buff=0.6)

        k_label = MathTex(r"k = 1, 2, 3, \ldots",
                          font_size=22, color=TEXT_C)
        k_label.next_to(formula, DOWN, buff=0.15)

        self.play(Write(formula), run_time=2.5)
        self.play(FadeIn(k_label), run_time=0.8)
        self.wait(2.5)