from big_ol_pile_of_manim_imports import *
from old_projects.clacks.question import Block
from old_projects.clacks.question import Wall
from old_projects.clacks.question import ClackFlashes


class PositionPhaseSpaceScene(Scene):
    CONFIG = {
        "rescale_coordinates": True,
        "wall_x": -6,
        "wall_config": {
            "height": 1.6,
            "tick_spacing": 0.35,
            "tick_length": 0.2,
        },
        "wall_height": 1.5,
        "floor_y": -3.5,
        "block1_config": {
            "mass": 10,
            "distance": 9,
            "velocity": -1,
            "width": 1.6,
        },
        "block2_config": {
            "mass": 1,
            "distance": 4,
        },
        "axes_config": {
            "x_min": -0.5,
            "x_max": 31,
            "y_min": -0.5,
            "y_max": 10.5,
            "x_axis_config": {
                "unit_size": 0.4,
                "tick_frequency": 2,
            },
            "y_axis_config": {
                "unit_size": 0.4,
                "tick_frequency": 2,
            },
        },
        "axes_center": 5 * LEFT + 0.65 * DOWN,
        "ps_dot_config": {
            "fill_color": RED,
            "background_stroke_width": 1,
            "background_stroke_color": BLACK,
            "radius": 0.05,
        },
        "ps_d2_label_vect": RIGHT,
        "ps_x_line_config": {
            "color": GREEN,
            "stroke_width": 2,
        },
        "ps_y_line_config": {
            "color": RED,
            "stroke_width": 2,
        },
        "clack_sound": "clack",
        "mirror_line_class": Line,
        "mirror_line_style": {
            "stroke_color": WHITE,
            "stroke_width": 1,
        },
        "d1_eq_d2_line_color": MAROON_B,
        "d1_eq_d2_tex": "d1 = d2",
        "trajectory_style": {
            "stroke_color": YELLOW,
            "stroke_width": 2,
        },
        "ps_velocity_vector_length": 0.75,
        "ps_velocity_vector_config": {
            "color": PINK,
            "rectangular_stem_width": 0.025,
            "tip_length": 0.15,
        },
        "block_velocity_vector_length_multiple": 2,
        "block_velocity_vector_config": {
            "color": PINK,
        },
    }

    def setup(self):
        self.total_sliding_time = 0
        self.all_items = [
            self.get_floor(),
            self.get_wall(),
            self.get_blocks(),
            self.get_axes(),
            self.get_phase_space_point(),
            self.get_phase_space_x_line(),
            self.get_phase_space_y_line(),
            self.get_phase_space_dot(),
            self.get_phase_space_d1_label(),
            self.get_phase_space_d2_label(),
            self.get_d1_brace(),
            self.get_d2_brace(),
            self.get_d1_label(),
            self.get_d2_label(),
            self.get_d1_eq_d2_line(),
            self.get_d1_eq_d2_label(),
            self.get_d2_eq_w2_line(),
            self.get_d2_eq_w2_label(),
        ]

    def get_floor_wall_corner(self):
        return self.wall_x * RIGHT + self.floor_y * UP

    def get_mass_ratio(self):
        return op.truediv(
            self.block1.mass,
            self.block2.mass,
        )

    def d1_to_x(self, d1):
        if self.rescale_coordinates:
            d1 *= np.sqrt(self.block1.mass)
        return d1

    def d2_to_y(self, d2):
        if self.rescale_coordinates:
            d2 *= np.sqrt(self.block2.mass)
        return d2

    def ds_to_point(self, d1, d2):
        return self.axes.coords_to_point(
            self.d1_to_x(d1), self.d2_to_y(d2),
        )

    def point_to_ds(self, point):
        x, y = self.axes.point_to_coords(point)
        if self.rescale_coordinates:
            x /= np.sqrt(self.block1.mass)
            y /= np.sqrt(self.block2.mass)
        return (x, y)

    def get_d1(self):
        return self.get_ds()[0]

    def get_d2(self):
        return self.get_ds()[1]

    def get_ds(self):
        return self.point_to_ds(self.ps_point.get_location())

    # Relevant for sliding
    def tie_blocks_to_ps_point(self):
        def update_blocks(blocks):
            d1, d2 = self.point_to_ds(self.ps_point.get_location())
            b1, b2 = blocks
            corner = self.get_floor_wall_corner()
            b1.move_to(corner + d1 * RIGHT, DL)
            b2.move_to(corner + d2 * RIGHT, DR)
        self.blocks.add_updater(update_blocks)

    def time_to_ds(self, time):
        # Deals in its own phase space, different
        # from the one displayed
        m1 = self.block1.mass
        m2 = self.block2.mass
        v1 = self.block1.velocity
        start_d1 = self.block1_config["distance"]
        start_d2 = self.block2_config["distance"]
        w2 = self.block2.width
        start_d2 += w2
        ps_speed = np.sqrt(m1) * abs(v1)
        theta = np.arctan(np.sqrt(m2 / m1))

        def ds_to_ps_point(d1, d2):
            return np.array([
                d1 * np.sqrt(m1),
                d2 * np.sqrt(m2),
                0
            ])

        def ps_point_to_ds(point):
            return (
                point[0] / np.sqrt(m1),
                point[1] / np.sqrt(m2),
            )

        ps_point = ds_to_ps_point(start_d1, start_d2)
        wedge_corner = ds_to_ps_point(w2, w2)
        ps_point -= wedge_corner
        # Pass into the mirror worlds
        ps_point += time * ps_speed * LEFT
        # Reflect back to the real world
        angle = angle_of_vector(ps_point)
        n = int(angle / theta)
        if n % 2 == 0:
            ps_point = rotate_vector(ps_point, -n * theta)
        else:
            ps_point = rotate_vector(
                ps_point,
                -(n + 1) * theta,
            )
            ps_point[1] = abs(ps_point[1])
        ps_point += wedge_corner
        return ps_point_to_ds(ps_point)

    def get_clack_data(self):
        # Copying from time_to_ds.  Not great, but
        # maybe I'll factor things out properly later.
        m1 = self.block1.mass
        m2 = self.block2.mass
        v1 = self.block1.velocity
        w2 = self.block2.get_width()
        h2 = self.block2.get_height()
        start_d1, start_d2 = self.get_ds()
        ps_speed = np.sqrt(m1) * abs(v1)
        theta = np.arctan(np.sqrt(m2 / m1))

        def ds_to_ps_point(d1, d2):
            return np.array([
                d1 * np.sqrt(m1),
                d2 * np.sqrt(m2),
                0
            ])

        def ps_point_to_ds(point):
            return (
                point[0] / np.sqrt(m1),
                point[1] / np.sqrt(m2),
            )

        ps_point = ds_to_ps_point(start_d1, start_d2)
        wedge_corner = ds_to_ps_point(w2, w2)
        ps_point -= wedge_corner
        y = ps_point[1]

        clack_data = []
        for k in range(1, int(PI / theta) + 1):
            clack_ps_point = np.array([
                y / np.tan(k * theta), y, 0
            ])
            time = get_norm(ps_point - clack_ps_point) / ps_speed
            reflected_point = rotate_vector(
                clack_ps_point,
                -2 * np.ceil((k - 1) / 2) * theta
            )
            d1, d2 = ps_point_to_ds(reflected_point + wedge_corner)
            loc1 = self.get_floor_wall_corner() + h2 * UP / 2 + d2 * RIGHT
            if k % 2 == 0:
                loc1 += w2 * LEFT
            loc2 = self.ds_to_point(d1, d2)
            clack_data.append((time, loc1, loc2))
        return clack_data

    def tie_ps_point_to_time_tracker(self):
        if not hasattr(self, "sliding_time_tracker"):
            self.sliding_time_tracker = self.get_time_tracker()

        def update_ps_point(p):
            time = self.sliding_time_tracker.get_value()
            ds = self.time_to_ds(time)
            p.move_to(self.ds_to_point(*ds))

        self.ps_point.add_updater(update_ps_point)
        self.add(self.sliding_time_tracker, self.ps_point)

    def add_clack_flashes(self):
        if hasattr(self, "flash_anims"):
            self.add(*self.flash_anims)
        else:
            clack_data = self.get_clack_data()
            self.clack_times = [
                time for (time, loc1, loc2) in clack_data
            ]
            self.block_flashes = ClackFlashes([
                (loc1, time)
                for (time, loc1, loc2) in clack_data
            ])
            self.ps_flashes = ClackFlashes([
                (loc2, time)
                for (time, loc1, loc2) in clack_data
            ])
            self.flash_anims = [self.block_flashes, self.ps_flashes]
            for anim in self.flash_anims:
                anim.get_time = self.sliding_time_tracker.get_value
            self.add(*self.flash_anims)

    def get_continually_building_trajectory(self):
        trajectory = VMobject()
        self.trajectory = trajectory
        trajectory.set_style(**self.trajectory_style)

        def get_point():
            return np.array(self.ps_point.get_location())

        points = [get_point(), get_point()]
        trajectory.set_points_as_corners(points)
        epsilon = 0.001

        def update_trajectory(trajectory):
            new_point = get_point()
            p1, p2 = trajectory.get_anchors()[-2:]
            angle = angle_between_vectors(
                p2 - p1,
                new_point - p2,
            )
            if angle > epsilon:
                points.append(new_point)
            else:
                points[-1] = new_point
            trajectory.set_points_as_corners(points)

        trajectory.add_updater(update_trajectory)
        return trajectory

    def begin_sliding(self, show_trajectory=True):
        self.tie_ps_point_to_time_tracker()
        self.add_clack_flashes()
        if show_trajectory:
            if hasattr(self, "trajectory"):
                self.trajectory.resume_updating()
            else:
                self.add(self.get_continually_building_trajectory())

    def end_sliding(self):
        self.update_mobjects(dt=0)
        self.ps_point.clear_updaters()
        if hasattr(self, "sliding_time_tracker"):
            self.remove(self.sliding_time_tracker)
        if hasattr(self, "flash_anims"):
            self.remove(*self.flash_anims)
        if hasattr(self, "trajectory"):
            self.trajectory.suspend_updating()
        old_total_sliding_time = self.total_sliding_time
        new_total_sliding_time = self.sliding_time_tracker.get_value()
        self.total_sliding_time = new_total_sliding_time
        for time in self.clack_times:
            if old_total_sliding_time < time < new_total_sliding_time:
                offset = time - new_total_sliding_time
                self.add_sound(
                    "clack",
                    time_offset=offset,
                )

    def slide(self, time, stop_condition=None):
        self.begin_sliding()
        self.wait(time, stop_condition)
        self.end_sliding()

    def slide_until(self, stop_condition, max_time=60):
        self.slide(max_time, stop_condition=stop_condition)

    def get_ps_point_change_anim(self, d1, d2, **added_kwargs):
        b1 = self.block1
        ps_speed = np.sqrt(b1.mass) * abs(b1.velocity)
        curr_d1, curr_d2 = self.get_ds()
        distance = get_norm([curr_d1 - d1, curr_d2 - d2])

        # Default
        kwargs = {
            "run_time": (distance / ps_speed),
            "rate_func": linear,
        }
        kwargs.update(added_kwargs)
        return ApplyMethod(
            self.ps_point.move_to,
            self.ds_to_point(d1, d2),
            **kwargs
        )

    # Mobject getters
    def get_floor(self):
        floor = self.floor = Line(
            self.wall_x * RIGHT,
            FRAME_WIDTH * RIGHT / 2,
            stroke_color=WHITE,
            stroke_width=3,
        )
        floor.move_to(self.get_floor_wall_corner(), LEFT)
        return floor

    def get_wall(self):
        wall = self.wall = Wall(**self.wall_config)
        wall.move_to(self.get_floor_wall_corner(), DR)
        return wall

    def get_blocks(self):
        blocks = self.blocks = VGroup()
        for n in [1, 2]:
            config = getattr(self, "block{}_config".format(n))
            block = Block(**config)
            block.move_to(self.get_floor_wall_corner(), DL)
            block.shift(config["distance"] * RIGHT)
            block.label.move_to(block)
            block.label.set_fill(BLACK)
            block.label.set_stroke(WHITE, 1, background=True)
            self.blocks.add(block)
        self.block1, self.block2 = blocks
        return blocks

    def get_axes(self):
        axes = self.axes = Axes(**self.axes_config)
        axes.set_stroke(LIGHT_GREY, 2)
        axes.shift(
            self.axes_center - axes.coords_to_point(0, 0)
        )
        axes.labels = self.get_axes_labels(axes)
        axes.add(axes.labels)
        axes.added_lines = self.get_added_axes_lines(axes)
        axes.add(axes.added_lines)
        return axes

    def get_added_axes_lines(self, axes):
        c2p = axes.coords_to_point
        x_mult = y_mult = 1
        if self.rescale_coordinates:
            x_mult = np.sqrt(self.block1.mass)
            y_mult = np.sqrt(self.block2.mass)
        y_lines = VGroup(*[
            Line(
                c2p(0, 0), c2p(0, axes.y_max * y_mult + 1),
            ).move_to(c2p(x, 0), DOWN)
            for x in np.arange(0, axes.x_max) * x_mult
        ])
        x_lines = VGroup(*[
            Line(
                c2p(0, 0), c2p(axes.x_max * x_mult, 0),
            ).move_to(c2p(0, y), LEFT)
            for y in np.arange(0, axes.y_max) * y_mult
        ])
        line_groups = VGroup(x_lines, y_lines)
        for lines in line_groups:
            lines.set_stroke(BLUE, 1, 0.5)
            lines[1::2].set_stroke(width=0.5, opacity=0.25)
        return line_groups

    def get_axes_labels(self, axes, with_sqrts=None):
        if with_sqrts is None:
            with_sqrts = self.rescale_coordinates
        x_label = TexMobject("x", "=", "d_1")
        y_label = TexMobject("y", "=", "d_2")
        labels = VGroup(x_label, y_label)
        if with_sqrts:
            additions = map(TexMobject, [
                "\\sqrt{m_1}", "\\sqrt{m_2}"
            ])
            for label, addition in zip(labels, additions):
                addition.move_to(label[2], DL)
                label[2].next_to(
                    addition, RIGHT, SMALL_BUFF,
                    aligned_edge=DOWN
                )
                addition[2:].set_color(BLUE)
                label.submobjects.insert(2, addition)
        x_label.next_to(axes.x_axis.get_right(), DL, MED_SMALL_BUFF)
        y_label.next_to(axes.y_axis.get_top(), DR, MED_SMALL_BUFF)
        for label in labels:
            label.shift_onto_screen()
        return labels

    def get_phase_space_point(self):
        ps_point = self.ps_point = VectorizedPoint()
        ps_point.move_to(self.ds_to_point(
            self.block1.distance,
            self.block2.distance + self.block2.width
        ))
        self.tie_blocks_to_ps_point()
        return ps_point

    def get_phase_space_x_line(self):
        def get_x_line():
            origin = self.axes.coords_to_point(0, 0)
            point = self.ps_point.get_location()
            y_axis_point = np.array(origin)
            y_axis_point[1] = point[1]
            return DashedLine(
                y_axis_point, point,
                **self.ps_x_line_config,
            )
        self.x_line = always_redraw(get_x_line)
        return self.x_line

    def get_phase_space_y_line(self):
        def get_y_line():
            origin = self.axes.coords_to_point(0, 0)
            point = self.ps_point.get_location()
            x_axis_point = np.array(origin)
            x_axis_point[0] = point[0]
            return DashedLine(
                x_axis_point, point,
                **self.ps_y_line_config,
            )
        self.y_line = always_redraw(get_y_line)
        return self.y_line

    def get_phase_space_dot(self):
        self.ps_dot = ps_dot = Dot(**self.ps_dot_config)
        ps_dot.add_updater(lambda m: m.move_to(self.ps_point))
        return ps_dot

    def get_d_label(self, n, get_d):
        label = VGroup(
            TexMobject("d_{}".format(n), "="),
            DecimalNumber(),
        )
        color = GREEN if n == 1 else RED
        label[0].set_color_by_tex("d_", color)
        label.scale(0.7)
        label.set_stroke(BLACK, 3, background=True)

        def update_value(label):
            lhs, rhs = label
            rhs.set_value(get_d())
            rhs.next_to(
                lhs, RIGHT, SMALL_BUFF,
                aligned_edge=DOWN,
            )
        label.add_updater(update_value)
        return label

    def get_phase_space_d_label(self, n, get_d, line, vect):
        label = self.get_d_label(n, get_d)
        label.add_updater(
            lambda m: m.next_to(line, vect, SMALL_BUFF)
        )
        return label

    def get_phase_space_d1_label(self):
        self.ps_d1_label = self.get_phase_space_d_label(
            1, self.get_d1, self.x_line, UP,
        )
        return self.ps_d1_label

    def get_phase_space_d2_label(self):
        self.ps_d2_label = self.get_phase_space_d_label(
            2, self.get_d2, self.y_line,
            self.ps_d2_label_vect,
        )
        return self.ps_d2_label

    def get_d_brace(self, get_right_point):
        line = Line(LEFT, RIGHT).set_width(6)

        def get_brace():
            right_point = get_right_point()
            left_point = np.array(right_point)
            left_point[0] = self.wall_x
            line.put_start_and_end_on(left_point, right_point)
            return Brace(line, UP, buff=SMALL_BUFF)

        brace = always_redraw(get_brace)
        return brace

    def get_d1_brace(self):
        self.d1_brace = self.get_d_brace(
            lambda: self.block1.get_corner(UL)
        )
        return self.d1_brace

    def get_d2_brace(self):
        self.d2_brace = self.get_d_brace(
            lambda: self.block2.get_corner(UR)
        )
        # self.flip_brace_nip()
        return self.d2_brace

    def flip_brace_nip(self, brace):
        nip_index = (len(brace) // 2) - 1
        nip = brace[nip_index:nip_index + 2]
        rect = brace[nip_index - 1]
        center = rect.get_center()
        center[0] = nip.get_center()[0]
        nip.rotate(PI, about_point=center)

    def get_brace_d_label(self, n, get_d, brace, vect, buff):
        label = self.get_d_label(n, get_d)
        label.add_updater(
            lambda m: m.next_to(brace, vect, buff)
        )
        return label

    def get_d1_label(self):
        self.d1_label = self.get_brace_d_label(
            1, self.get_d1, self.d1_brace, UP, SMALL_BUFF,
        )
        return self.d1_label

    def get_d2_label(self):
        self.d2_label = self.get_brace_d_label(
            2, self.get_d2, self.d2_brace, UP, 0
        )
        return self.d2_label

    def get_d1_eq_d2_line(self):
        start = self.ds_to_point(0, 0)
        end = self.ds_to_point(15, 15)
        line = self.d1_eq_d2_line = self.mirror_line_class(start, end)
        line.set_style(**self.mirror_line_style)
        line.set_color(self.d1_eq_d2_line_color)
        return self.d1_eq_d2_line

    def get_d1_eq_d2_label(self):
        label = TexMobject(self.d1_eq_d2_tex)
        label.scale(0.75)
        line = self.d1_eq_d2_line
        point = interpolate(
            line.get_start(), line.get_end(),
            0.7,
        )
        label.next_to(point, DR, SMALL_BUFF)
        label.match_color(line)
        label.set_stroke(BLACK, 5, background=True)
        self.d1_eq_d2_label = label
        return label

    def get_d2_eq_w2_line(self):
        w2 = self.block2.width
        start = self.ds_to_point(0, w2)
        end = np.array(start)
        end[0] = FRAME_WIDTH / 2
        self.d2_eq_w2_line = self.mirror_line_class(start, end)
        self.d2_eq_w2_line.set_style(**self.mirror_line_style)
        return self.d2_eq_w2_line

    def get_d2_eq_w2_label(self):
        label = TexMobject("d2 = \\text{block width}")
        label.scale(0.75)
        label.next_to(self.d2_eq_w2_line, UP, SMALL_BUFF)
        label.to_edge(RIGHT, buff=MED_SMALL_BUFF)
        self.d2_eq_w2_label = label
        return label

    def get_time_tracker(self, time=0):
        time_tracker = self.time_tracker = ValueTracker(time)
        time_tracker.add_updater(
            lambda m, dt: m.increment_value(dt)
        )
        return time_tracker

    # Things associated with velocity
    def get_ps_velocity_vector(self, trajectory):
        vector = Vector(
            self.ps_velocity_vector_length * LEFT,
            **self.ps_velocity_vector_config,
        )

        def update_vector(v):
            anchors = trajectory.get_anchors()
            index = len(anchors) - 2
            vect = np.array(ORIGIN)
            while get_norm(vect) == 0 and index > 0:
                p0, p1 = anchors[index:index + 2]
                vect = p1 - p0
                index -= 1
            angle = angle_of_vector(vect)
            point = self.ps_point.get_location()
            v.set_angle(angle)
            v.shift(point - v.get_start())
        vector.add_updater(update_vector)
        self.ps_velocity_vector = vector
        return vector

    def get_block_velocity_vectors(self, ps_vect):
        blocks = self.blocks
        vectors = VGroup(*[
            Vector(LEFT, **self.block_velocity_vector_config)
            for x in range(2)
        ])
        # TODO: Put in config
        vectors[0].set_color(GREEN)
        vectors[1].set_color(RED)

        def update_vectors(vs):
            v_2d = ps_vect.get_vector()[:2]
            v_2d *= self.block_velocity_vector_length_multiple
            for v, coord, block in zip(vs, v_2d, blocks):
                v.put_start_and_end_on(ORIGIN, coord * RIGHT)
                start = block.get_edge_center(v.get_vector())
                v.shift(start)
        vectors.add_updater(update_vectors)

        self.block_velocity_vectors = vectors
        return vectors


class IntroducePositionPhaseSpace(PositionPhaseSpaceScene):
    CONFIG = {
        "rescale_coordinates": False,
        "d1_eq_d2_tex": "x = y",
        "block1_config": {
            "velocity": 1.5,
        },
        "slide_wait_time": 12,
    }

    def setup(self):
        super().setup()
        self.add(
            self.floor,
            self.wall,
            self.blocks,
            self.axes,
        )

    def construct(self):
        self.show_coordinates()
        self.show_xy_line()
        self.let_process_play_out()
        self.show_w2_line()

    def show_coordinates(self):
        ps_point = self.ps_point
        axes = self.axes

        self.play(Write(axes.added_lines))
        self.play(FadeInFromLarge(self.ps_dot, scale_factor=10))
        self.play(
            ShowCreation(self.x_line),
            GrowFromPoint(
                self.d1_brace,
                self.d1_brace.get_left(),
            ),
            Indicate(axes.labels[0]),
        )
        self.play(
            FadeInFromDown(self.ps_d1_label),
            FadeInFromDown(self.d1_label),
        )
        self.play(ps_point.shift, 0.5 * LEFT)
        self.play(ps_point.shift, 0.5 * RIGHT)
        self.wait()
        self.play(
            ShowCreation(self.y_line),
            GrowFromPoint(
                self.d2_brace,
                self.d2_brace.get_left(),
            ),
            Indicate(axes.labels[1]),
        )
        self.play(
            FadeInFromDown(self.ps_d2_label),
            FadeInFromDown(self.d2_label),
        )
        self.play(ps_point.shift, 0.5 * UP)
        self.play(ps_point.shift, 0.5 * DOWN)
        self.wait()
        self.play(Rotating(
            ps_point,
            about_point=ps_point.get_location() + 0.5 * RIGHT,
            run_time=3,
            rate_func=smooth,
        ))
        self.wait()

    def show_xy_line(self):
        ps_point = self.ps_point
        ps_point.save_state()
        d1, d2 = self.point_to_ds(ps_point.get_location())

        xy_line = self.d1_eq_d2_line
        xy_label = self.d1_eq_d2_label

        self.play(
            ShowCreation(xy_line),
            Write(xy_label),
        )
        self.play(
            ps_point.move_to, self.ds_to_point(d2, d2),
            run_time=3
        )
        self.wait()
        for d in [3, 7]:
            self.play(
                ps_point.move_to, self.ds_to_point(d, d),
                run_time=2
            )
            self.wait()
        self.play(ps_point.restore)
        self.wait()

    def let_process_play_out(self):
        self.begin_sliding()
        sliding_trajectory = self.get_continually_building_trajectory()
        self.add(sliding_trajectory, self.ps_dot)
        self.wait(self.slide_wait_time)

    def show_w2_line(self):
        line = self.d2_eq_w2_line
        label = self.d2_eq_w2_label

        self.play(ShowCreation(line))
        self.play(FadeInFromDown(label))
        self.wait(self.slide_wait_time)
        self.end_sliding()
        self.wait(self.slide_wait_time)


class SpecialShowPassingFlash(ShowPassingFlash):
    CONFIG = {
        "max_time_width": 0.1,
    }

    def get_bounds(self, alpha):
        tw = self.time_width
        max_tw = self.max_time_width
        upper = interpolate(0, 1 + max_tw, alpha)
        lower = upper - tw
        upper = min(upper, 1)
        lower = max(lower, 0)
        return (lower, upper)


class EqualMassCase(PositionPhaseSpaceScene):
    CONFIG = {
        "block1_config": {
            "mass": 1,
            "width": 1,
            "velocity": 1.5,
        },
        "rescale_coordinates": False,
        "d1_eq_d2_tex": "x = y",
    }

    def setup(self):
        super().setup()
        self.add(
            self.floor,
            self.wall,
            self.blocks,
            self.axes,
            self.d1_eq_d2_line,
            self.d1_eq_d2_label,
            self.d2_eq_w2_line,
            self.d2_eq_w2_label,
            self.ps_dot,
            self.x_line,
            self.y_line,
            self.ps_d1_label,
            self.ps_d2_label,
        )

    def construct(self):
        self.show_same_mass()
        self.show_first_point()
        self.up_to_first_collision()
        self.up_to_second_collision()
        self.up_to_third_collision()

        self.fade_distance_indicators()
        self.show_beam_bouncing()

    def show_same_mass(self):
        blocks = self.blocks
        self.play(LaggedStartMap(
            Indicate, blocks,
            lag_ratio=0.8,
            run_time=1,
        ))

    def show_first_point(self):
        ps_dot = self.ps_dot
        ps_point = self.ps_point
        d1, d2 = self.get_ds()

        self.play(FocusOn(ps_dot))
        self.play(ShowCreationThenFadeOut(
            Circle(color=RED).replace(ps_dot).scale(2),
            run_time=1
        ))
        self.wait()
        self.play(
            ps_point.move_to, self.ds_to_point(d1 - 1, d2),
            rate_func=wiggle,
            run_time=3,
        )
        # self.play(ps_point.move_to, self.ds_to_point(d1, d2))
        self.wait()

    def up_to_first_collision(self):
        ps_point = self.ps_point
        d1, d2 = self.get_ds()
        block1 = self.block1
        block2 = self.block2
        xy_line = self.d1_eq_d2_line
        xy_line_label = self.d1_eq_d2_label

        block_arrow = Vector(LEFT, color=RED)
        block_arrow.block = block1
        block_arrow.add_updater(
            lambda m: m.shift(
                m.block.get_center() - m.get_start()
            )
        )
        ps_arrow = Vector(LEFT, color=RED)
        ps_arrow.next_to(ps_point, DL, buff=SMALL_BUFF)

        block_labels = VGroup(block1.label, block2.label)
        block_label_copies = block_labels.copy()

        def update_bl_copies(bl_copies):
            for bc, b in zip(bl_copies, block_labels):
                bc.move_to(b)
        block_label_copies.add_updater(update_bl_copies)

        trajectory = self.get_continually_building_trajectory()

        self.add(block_arrow, ps_arrow, block_label_copies)
        self.play(
            GrowArrow(block_arrow),
            GrowArrow(ps_arrow),
        )
        self.add(trajectory)
        self.play(self.get_ps_point_change_anim(d2, d2))
        block_arrow.block = block2
        ps_arrow.rotate(90 * DEGREES)
        ps_arrow.next_to(ps_point, DR, SMALL_BUFF)
        self.add_sound(self.clack_sound)
        self.play(
            Flash(ps_point),
            Flash(block1.get_left()),
            self.get_ps_point_change_anim(d2, d2 - 1)
        )
        self.play(
            ShowPassingFlash(
                xy_line.copy().set_stroke(YELLOW, 3)
            ),
            Indicate(xy_line_label),
        )

        trajectory.suspend_updating()
        self.wait()

        self.ps_arrow = ps_arrow
        self.block_arrow = block_arrow

    def up_to_second_collision(self):
        trajectory = self.trajectory
        ps_point = self.ps_point
        ps_arrow = self.ps_arrow
        block_arrow = self.block_arrow

        d1, d2 = self.get_ds()
        w2 = self.block2.get_width()

        trajectory.resume_updating()
        self.play(self.get_ps_point_change_anim(d1, w2))
        block_arrow.rotate(PI)
        ps_arrow.rotate(PI)
        ps_arrow.next_to(ps_point, UR, SMALL_BUFF)
        self.add_sound(self.clack_sound)
        self.play(
            Flash(self.block2.get_left()),
            Flash(ps_point),
            self.get_ps_point_change_anim(d1, w2 + 1)
        )

        trajectory.suspend_updating()
        self.wait()

    def up_to_third_collision(self):
        trajectory = self.trajectory
        ps_point = self.ps_point
        ps_arrow = self.ps_arrow
        block_arrow = self.block_arrow
        d1, d2 = self.get_ds()

        trajectory.resume_updating()
        self.play(self.get_ps_point_change_anim(d1, d1))
        block_arrow.block = self.block1
        ps_arrow.rotate(-90 * DEGREES)
        ps_arrow.next_to(ps_point, DR, SMALL_BUFF)
        self.add_sound(self.clack_sound)
        self.play(
            Flash(self.block2.get_left()),
            Flash(ps_point.get_location()),
            self.get_ps_point_change_anim(d1 + 10, d1)
        )
        trajectory.suspend_updating()

    def fade_distance_indicators(self):
        trajectory = self.trajectory
        self.play(
            trajectory.set_stroke, {"width": 1},
            *map(FadeOut, [
                self.ps_arrow,
                self.block_arrow,
                self.x_line,
                self.y_line,
                self.ps_d1_label,
                self.ps_d2_label,
            ])
        )
        trajectory.clear_updaters()

    def show_beam_bouncing(self):
        d1, d2 = self.get_ds()
        d1 = int(d1)
        d2 = int(d2)
        # w2 = self.block2.get_width()
        ps_point = self.ps_point

        points = []
        while d1 > d2:
            points.append(self.ds_to_point(d1, d2))
            d1 -= 1
        while d2 >= 1:
            points.append(self.ds_to_point(d1, d2))
            d2 -= 1
        points += list(reversed(points))[1:]
        trajectory = VMobject()
        trajectory.set_points_as_corners(points)
        flashes = [
            SpecialShowPassingFlash(
                trajectory.copy().set_stroke(YELLOW, width=6 - n),
                time_width=(0.01 * n),
                max_time_width=0.05,
                remover=True
            )
            for n in np.arange(0, 6, 0.25)
        ]
        flash_mob = flashes[0].mobject  # Lol

        def update_ps_point_from_flas_mob(ps_point):
            if len(flash_mob.points) > 0:
                ps_point.move_to(flash_mob.points[-1])
            else:
                ps_point.move_to(trajectory.points[0])

        # Mirror words
        xy_line = self.d1_eq_d2_line
        w2_line = self.d2_eq_w2_line
        lines = VGroup(xy_line, w2_line)
        for line in lines:
            word = TextMobject("Mirror")
            word.next_to(ORIGIN, UP, SMALL_BUFF)
            word.rotate(line.get_angle(), about_point=ORIGIN)
            word.shift(line.get_center())
            line.word = word

        for line in lines:
            line.set_stroke(LIGHT_GREY)
            line.set_sheen(1, LEFT)
            self.play(
                Write(line.word),
                line.set_sheen, 1, RIGHT,
                line.set_stroke, {"width": 2},
                run_time=1,
            )

        # TODO, clacks?
        for x in range(3):
            self.play(
                UpdateFromFunc(
                    ps_point,
                    update_ps_point_from_flas_mob,
                ),
                *flashes,
                run_time=3,
                rate_func=linear,
            )
            self.wait()


class FailedAngleRelation(PositionPhaseSpaceScene):
    CONFIG = {
        "block1_config": {
            "distance": 10,
            "velocity": -1.5,
        },
        "block2_config": {
            "distance": 5,
        },
        "rescale_coordinates": False,
        "trajectory_style": {
            "stroke_width": 2,
        }
    }

    def setup(self):
        super().setup()
        self.add(
            self.floor,
            self.wall,
            self.blocks,
            self.axes,
            self.ps_dot,
            self.x_line,
            self.y_line,
            self.d1_eq_d2_line,
            self.d1_eq_d2_label,
            self.d2_eq_w2_line,
            self.d2_eq_w2_label,
        )

    def construct(self):
        self.show_first_collision()
        self.show_angles()

    def show_first_collision(self):
        self.slide_until(lambda: self.get_ds()[1] < 2)

    def show_angles(self):
        trajectory = self.trajectory
        arcs = self.get_arcs(trajectory)
        equation = self.get_word_equation()
        equation.next_to(
            trajectory.points[0], UR, MED_SMALL_BUFF,
            index_of_submobject_to_align=0,
        )

        for arc in arcs:
            line = Line(ORIGIN, RIGHT)
            line.set_stroke(WHITE, 2)
            line.rotate(arc.start_angle)
            line.shift(arc.arc_center - line.get_start())
            arc.line = line

        arc1, arc2 = arcs
        arc1.arrow = Arrow(
            equation[0].get_left(), arc1.get_right(),
            buff=SMALL_BUFF,
            color=WHITE,
            path_arc=0,
        )
        arc2.arrow = Arrow(
            equation[2].get_corner(DL),
            arc2.get_left(),
            path_arc=-120 * DEGREES,
            buff=SMALL_BUFF,
        )
        arc2.arrow.pointwise_become_partial(arc.arrow, 0, 0.95)

        arc1.word = equation[0]
        arc2.word = equation[1:]

        for arc in arcs:
            self.play(
                FadeInFrom(arc.word, LEFT),
                GrowArrow(arc.arrow, path_arc=arc.arrow.path_arc),
            )
            self.play(
                ShowCreation(arc),
                arc.line.rotate, arc.angle,
                {"about_point": arc.line.get_start()},
                UpdateFromAlphaFunc(
                    arc.line,
                    lambda m, a: m.set_stroke(
                        opacity=(there_and_back(a)**0.5)
                    )
                ),
            )

    #
    def get_arcs(self, trajectory):
        p0, p1, p2 = trajectory.get_anchors()[1:4]
        arc_config = {
            "stroke_color": WHITE,
            "stroke_width": 2,
            "radius": 0.5,
            "arc_center": p1,
        }
        arc1 = Arc(
            start_angle=0,
            angle=45 * DEGREES,
            **arc_config
        )
        a2_start = angle_of_vector(DL)
        a2_angle = angle_between_vectors((p2 - p1), DL)
        arc2 = Arc(
            start_angle=a2_start,
            angle=a2_angle,
            **arc_config
        )
        return VGroup(arc1, arc2)

    def get_word_equation(self):
        result = VGroup(
            TextMobject("Angle of incidence"),
            TexMobject("\\ne").rotate(90 * DEGREES),
            TextMobject("Angle of reflection")
        )
        result.arrange(DOWN)
        result.set_stroke(BLACK, 5, background=True)
        return result


class UnscaledPositionPhaseSpaceMass10(FailedAngleRelation):
    CONFIG = {
        "block1_config": {
            "mass": 10
        },
        "wait_time": 25,
    }

    def construct(self):
        self.slide(self.wait_time)


class UnscaledPositionPhaseSpaceMass100(UnscaledPositionPhaseSpaceMass10):
    CONFIG = {
        "block1_config": {
            "mass": 100
        }
    }


class RescaleCoordinates(PositionPhaseSpaceScene, MovingCameraScene):
    CONFIG = {
        "rescale_coordinates": False,
        "ps_d2_label_vect": LEFT,
        "axes_center": 6 * LEFT + 0.65 * DOWN,
        "block1_config": {"distance": 7},
        "wait_time": 30,
    }

    def setup(self):
        PositionPhaseSpaceScene.setup(self)
        MovingCameraScene.setup(self)
        self.add(
            self.floor,
            self.wall,
            self.blocks,
            self.axes,
            self.d1_eq_d2_line,
            self.d1_eq_d2_label,
            self.d2_eq_w2_line,
            self.ps_dot,
            self.x_line,
            self.y_line,
            self.ps_d1_label,
            self.ps_d2_label,
            self.d1_brace,
            self.d2_brace,
            self.d1_label,
            self.d2_label,
        )

    def construct(self):
        self.show_rescaling()
        self.comment_on_ugliness()
        self.put_into_frame()

    def show_rescaling(self):
        axes = self.axes
        blocks = self.blocks
        to_stretch = VGroup(
            axes.added_lines,
            self.d1_eq_d2_line,
            self.ps_point,
        )
        m1 = self.block1.mass
        new_axes_labels = self.get_axes_labels(axes, with_sqrts=True)

        # Show label
        def show_label(index, block, vect):
            self.play(
                ShowCreationThenFadeAround(axes.labels[index])
            )
            self.play(
                Transform(
                    axes.labels[index],
                    VGroup(
                        *new_axes_labels[index][:2],
                        new_axes_labels[index][3]
                    ),
                ),
                GrowFromCenter(new_axes_labels[index][2])
            )
            group = VGroup(
                new_axes_labels[index][2][-2:].copy(),
                TexMobject("="),
                block.label.copy(),
            )
            group.generate_target()
            group.target.arrange(RIGHT, buff=SMALL_BUFF)
            group.target.next_to(block, vect)
            group[1].scale(0)
            group[1].move_to(group.target[1])
            group.target[2].set_fill(WHITE)
            group.target[2].set_stroke(width=0, background=True)
            self.play(MoveToTarget(
                group,
                rate_func=there_and_back_with_pause,
                run_time=3
            ))
            self.remove(group)
            self.wait()

        show_label(0, self.block1, RIGHT)

        # The stretch
        blocks.suspend_updating()
        self.play(
            ApplyMethod(
                to_stretch.stretch, np.sqrt(m1), 0,
                {"about_point": axes.coords_to_point(0, 0)},
            ),
            self.d1_eq_d2_label.shift, 6 * RIGHT,
            run_time=2,
        )
        self.rescale_coordinates = True
        blocks.resume_updating()
        self.wait()

        # Show wiggle
        d1, d2 = self.get_ds()
        for new_d1 in [d1 - 2, d1]:
            self.play(self.get_ps_point_change_anim(
                new_d1, d2,
                rate_func=smooth,
                run_time=2,
            ))
        self.wait()

        # Change y-coord
        show_label(1, self.block2, LEFT)

        axes.remove(axes.labels)
        self.remove(axes.labels)
        axes.labels = new_axes_labels
        axes.add(axes.labels)
        self.add(axes)

    def comment_on_ugliness(self):
        axes = self.axes

        randy = Randolph(height=1.7)
        randy.flip()
        randy.next_to(self.d2_eq_w2_line, UP, buff=0)
        randy.to_edge(RIGHT)
        randy.change("sassy")
        randy.save_state()
        randy.fade(1)
        randy.change("plain")

        self.play(Restore(randy))
        self.play(
            PiCreatureSays(
                randy, "Hideous!",
                bubble_kwargs={"height": 1.5, "width": 2},
                target_mode="angry",
                look_at_arg=axes.labels[0]
            )
        )
        self.play(randy.look_at, axes.labels[1])
        self.play(Blink(randy))
        self.play(
            RemovePiCreatureBubble(
                randy, target_mode="confused"
            )
        )
        self.play(Blink(randy))
        self.play(randy.look_at, axes.labels[0])
        self.wait()
        self.play(FadeOut(randy))

    def put_into_frame(self):
        rect = ScreenRectangle(height=FRAME_HEIGHT + 10)
        inner_rect = ScreenRectangle(height=FRAME_HEIGHT)
        rect.add_subpath(inner_rect.points[::-1])
        rect.set_fill("#333333", opacity=1)
        frame = self.camera_frame

        self.begin_sliding()
        self.add(rect)
        self.play(
            frame.scale, 1.5,
            {"about_point": frame.get_bottom() + UP},
            run_time=2,
        )
        self.wait(self.wait_time)
        self.end_sliding()

    #
    def get_ds(self):
        if self.rescale_coordinates:
            return super().get_ds()
        return (
            self.block1_config["distance"],
            self.block2_config["distance"],
        )


class RescaleCoordinatesMass16(RescaleCoordinates):
    CONFIG = {
        "block1_config": {
            "mass": 16,
            "distance": 10,
        },
        "rescale_coordinates": True,
        "wait_time": 20,
    }

    def construct(self):
        self.put_into_frame()


class RescaleCoordinatesMass64(RescaleCoordinatesMass16):
    CONFIG = {
        "block1_config": {
            "mass": 64,
            "distance": 6,
        },
        "block2_config": {"distance": 3},
    }


class RescaleCoordinatesMass100(RescaleCoordinatesMass16):
    CONFIG = {
        "block1_config": {
            "mass": 100,
            "distance": 6,
            "velocity": 0.5,
        },
        "block2_config": {"distance": 2},
        "wait_time": 25,
    }


class IntroduceVelocityVector(PositionPhaseSpaceScene, MovingCameraScene):
    CONFIG = {
        "zoom": True,
        "ps_x_line_config": {
            "color": WHITE,
            "stroke_width": 1,
            "stroke_opacity": 0.5,
        },
        "ps_y_line_config": {
            "color": WHITE,
            "stroke_width": 1,
            "stroke_opacity": 0.5,
        },
        "axes_center": 6 * LEFT + 0.65 * DOWN,
        "slide_time": 20,
        "new_vect_config": {
            "tip_length": 0.1,
            "rectangular_stem_width": 0.02,
        }
    }

    def setup(self):
        MovingCameraScene.setup(self)
        PositionPhaseSpaceScene.setup(self)
        self.add(
            self.floor,
            self.wall,
            self.blocks,
            self.axes,
            self.d1_eq_d2_line,
            self.d1_eq_d2_label,
            self.d2_eq_w2_line,
            self.x_line,
            self.y_line,
            self.ps_dot,
        )

    def construct(self):
        self.show_velocity_vector()
        self.contrast_with_physical_velocities()
        self.zoom_in_on_vector()
        self.break_down_components()
        self.zoom_out()
        self.relate_x_dot_y_dot_to_v1_v2()
        self.calculate_magnitude()
        self.let_process_play_out()

    def show_velocity_vector(self):
        self.slide(2)
        ps_vect = self.get_ps_velocity_vector(self.trajectory)
        self.play(GrowArrow(ps_vect))
        self.play(ShowCreationThenFadeAround(ps_vect))
        self.wait()

    def contrast_with_physical_velocities(self):
        ps_vect = self.ps_velocity_vector
        block_vectors = self.get_block_velocity_vectors(ps_vect)

        self.play(LaggedStartMap(GrowArrow, block_vectors))
        self.play(Rotating(
            ps_vect,
            angle=TAU,
            about_point=ps_vect.get_start(),
            run_time=5,
            rate_func=smooth,
        ))
        self.wait()
        self.slide_until(lambda: self.get_d2() < 2.5)

    def zoom_in_on_vector(self):
        if not self.zoom:
            self.wait(3)
            return
        ps_vect = self.ps_velocity_vector
        new_vect = Arrow(
            ps_vect.get_start(),
            ps_vect.get_end(),
            buff=0,
            **self.new_vect_config
        )
        new_vect.match_style(ps_vect)

        camera_frame = self.camera_frame
        camera_frame.save_state()
        point = self.ps_point.get_location()
        point += MED_SMALL_BUFF * DOWN
        self.play(
            camera_frame.scale, 0.25, {"about_point": point},
            Transform(ps_vect, new_vect),
            run_time=2,
        )
        self.wait()

    def break_down_components(self):
        # Create vectors
        ps_vect = self.ps_velocity_vector
        start = ps_vect.get_start()
        end = ps_vect.get_end()
        ul_corner = np.array(start)
        dr_corner = np.array(start)
        ul_corner[0] = end[0]
        dr_corner[1] = end[1]

        x_vect = Arrow(
            start, ul_corner,
            buff=0,
            **self.new_vect_config
        )
        y_vect = Arrow(
            start, dr_corner,
            buff=0,
            **self.new_vect_config
        )
        x_vect.set_fill(GREEN, opacity=0.75)
        y_vect.set_fill(RED, opacity=0.75)
        vects = VGroup(x_vect, y_vect)

        # Projection lines
        x_line, y_line = [
            DashedLine(
                ps_vect.get_end(),
                vect.get_end(),
                dash_length=0.01,
                color=vect.get_color(),
            )
            for vect in (x_vect, y_vect)
        ]
        self.projection_lines = VGroup(x_line, y_line)

        # Vector labels
        dx_label = TexMobject("\\frac{dx}{dt}")
        dy_label = TexMobject("\\frac{dy}{dt}")
        labels = VGroup(dx_label, dy_label)
        for label, arrow, direction in zip(labels, vects, [UP, RIGHT]):
            label.scale(0.25)
            buff = 0.25 * SMALL_BUFF
            label.next_to(arrow, direction, buff=buff)
        label.set_stroke(BLACK, 3, background=True)

        if not self.zoom:
            self.grow_labels(labels)

        self.play(
            TransformFromCopy(ps_vect, x_vect),
            ShowCreation(x_line),
        )
        self.play(FadeInFrom(dx_label, 0.25 * DOWN))
        self.wait()
        self.play(
            TransformFromCopy(ps_vect, y_vect),
            ShowCreation(y_line),
        )
        self.play(FadeInFrom(dy_label, 0.25 * LEFT))
        self.wait()

        # Ask about dx_dt
        randy = Randolph()
        randy.match_height(dx_label)
        randy.next_to(dx_label, LEFT, SMALL_BUFF)
        randy.change("confused", dx_label)
        randy.save_state()
        randy.fade(1)
        randy.change("plain")

        self.play(Restore(randy))
        self.play(WiggleOutThenIn(dx_label))
        self.play(Blink(randy))
        self.play(FadeOut(randy))

        self.derivative_labels = labels
        self.component_vectors = vects

    def zoom_out(self):
        if not self.zoom:
            self.wait(2)
            return
        labels = self.derivative_labels
        self.play(
            Restore(self.camera_frame),
            ApplyFunction(self.grow_labels, labels),
            run_time=2
        )

    def relate_x_dot_y_dot_to_v1_v2(self):
        derivative_labels = self.derivative_labels.copy()
        dx_label, dy_label = derivative_labels
        x_label, y_label = self.axes.labels
        m_part = x_label[2]
        block1 = self.block1
        block_vectors = self.block_velocity_vectors

        x_eq = x_label[1]
        dx_eq = TexMobject("=")
        dx_eq.next_to(
            x_eq, DOWN,
            buff=LARGE_BUFF,
            aligned_edge=RIGHT,
        )
        for label in derivative_labels:
            label.generate_target()
            label.target.scale(1.5)
        dx_label.target.next_to(dx_eq, LEFT)
        dx_rhs = TexMobject("\\sqrt{m_1}", "v_1")
        dx_rhs[0][2:].set_color(BLUE)
        dx_rhs[1].set_color(GREEN)
        dx_rhs.next_to(dx_eq, RIGHT)
        alt_v1 = dx_rhs[1].copy()

        self.play(ShowCreationThenFadeAround(x_label))
        self.play(MoveToTarget(dx_label))
        self.play(TransformFromCopy(x_eq, dx_eq))
        self.wait()
        self.play(
            VGroup(block1, m_part).shift, SMALL_BUFF * UP,
            rate_func=wiggle,
        )
        self.wait()
        self.d1_brace.update()
        self.d1_label.update()
        self.play(
            ShowCreationThenFadeAround(x_label[3]),
            FadeIn(self.d1_brace),
            FadeIn(self.d1_label),
        )
        self.wait()
        self.play(
            TransformFromCopy(x_label[3], dx_rhs[1]),
            TransformFromCopy(x_label[2], VGroup(dx_rhs[0])),
        )
        block_vectors.suspend_updating()
        self.play(alt_v1.next_to, block_vectors[0], UP, SMALL_BUFF)
        self.play(
            Rotate(
                block_vectors[0], 10 * DEGREES,
                about_point=block_vectors[0].get_start(),
                rate_func=wiggle,
                run_time=1,
            )
        )
        self.play(FadeOut(alt_v1))
        block_vectors.resume_updating()
        self.wait()

        # dy_label
        y_eq = y_label[1]
        dy_eq = TexMobject("=")
        dy_eq.next_to(y_eq, DOWN, LARGE_BUFF)
        dy_label.target.next_to(dy_eq, LEFT)
        dy_rhs = TexMobject("\\sqrt{m_2}", "v_2")
        dy_rhs[0][2:].set_color(BLUE)
        dy_rhs[1].set_color(RED)
        dy_rhs.next_to(dy_eq, RIGHT)
        VGroup(dy_label.target, dy_eq, dy_rhs).align_to(y_label, LEFT)
        alt_v2 = dy_rhs[1].copy()
        self.play(MoveToTarget(dy_label))
        self.play(
            Write(dy_eq),
            Write(dy_rhs),
        )
        self.play(alt_v2.next_to, block_vectors[1], UP, SMALL_BUFF)
        self.wait()
        self.play(FadeOut(alt_v2))
        self.wait()

        self.derivative_equations = VGroup(
            VGroup(dx_label, dx_eq, dx_rhs),
            VGroup(dy_label, dy_eq, dy_rhs),
        )

    def calculate_magnitude(self):
        corner_rect = Rectangle(
            stroke_color=WHITE,
            stroke_width=1,
            fill_color=BLACK,
            fill_opacity=1,
            height=2.5,
            width=8.5,
        )
        corner_rect.to_corner(UR, buff=0)

        ps_vect = self.ps_velocity_vector
        big_ps_vect = Arrow(
            ps_vect.get_start(), ps_vect.get_end(),
            buff=0,
        )
        big_ps_vect.match_style(ps_vect)
        big_ps_vect.scale(1.5)
        magnitude_bars = TexMobject("||", "||")
        magnitude_bars.match_height(
            big_ps_vect, stretch=True
        )
        rhs_scale_val = 0.8
        rhs = TexMobject(
            "=\\sqrt{"
            "\\left( dx/dt \\right)^2 + "
            "\\left( dy/dt \\right)^2"
            "}"
        )
        rhs.scale(rhs_scale_val)
        group = VGroup(
            magnitude_bars[0], big_ps_vect,
            magnitude_bars[1], rhs
        )
        group.arrange(RIGHT)
        group.next_to(corner_rect.get_corner(UL), DR)

        new_rhs = TexMobject(
            "=", "\\sqrt", "{m_1(v_1)^2 + m_2(v_2)^2}",
            tex_to_color_map={
                "m_1": BLUE,
                "m_2": BLUE,
                "v_1": GREEN,
                "v_2": RED,
            }
        )
        new_rhs.scale(rhs_scale_val)
        new_rhs.next_to(rhs, DOWN, aligned_edge=LEFT)

        final_rhs = TexMobject(
            "=", "\\sqrt{2(\\text{Kinetic energy})}"
        )
        final_rhs.scale(rhs_scale_val)
        final_rhs.next_to(new_rhs, DOWN, aligned_edge=LEFT)

        self.play(
            FadeIn(corner_rect),
            TransformFromCopy(ps_vect, big_ps_vect)
        )
        self.play(Write(magnitude_bars), Write(rhs[0]))
        self.wait()
        self.play(Write(rhs[1:]))
        self.wait()
        self.play(FadeInFrom(new_rhs, UP))
        for equation in self.derivative_equations:
            self.play(ShowCreationThenFadeAround(equation))
        self.wait()
        self.play(FadeInFrom(final_rhs, UP))
        self.wait()

    def let_process_play_out(self):
        self.play(*map(FadeOut, [
            self.projection_lines,
            self.derivative_labels,
            self.component_vectors,
            self.d1_brace,
            self.d1_label,
        ]))
        self.add(self.blocks, self.derivative_equations)
        self.blocks.resume_updating()
        self.slide(self.slide_time)

    #
    def grow_labels(self, labels):
        for label, vect in zip(labels, [DOWN, LEFT]):
            p = label.get_edge_center(vect)
            p += SMALL_BUFF * vect
            label.scale(2.5, about_point=p)
        return labels


class IntroduceVelocityVectorWithoutZoom(IntroduceVelocityVector):
    CONFIG = {
        "zoom": False,
    }


class ShowMomentumConservation(IntroduceVelocityVector):
    CONFIG = {
        "ps_velocity_vector_length": 1.25,
        "block_velocity_vector_length_multiple": 1,
        "block1_config": {
            "distance": 7,
        },
        "axes_config": {
            "y_max": 11,
        },
        "axes_center": 6.5 * LEFT + 1.2 * DOWN,
        "floor_y": -3.75,
        "wait_time": 15,
    }

    def construct(self):
        self.add_velocity_vectors()
        self.contrast_d1_d2_line_with_xy_line()
        self.rearrange_for_slope()
        self.up_to_first_collision()
        self.ask_what_next()
        self.show_conservation_of_momentum()
        self.show_rate_of_change_vector()
        self.show_sqrty_m_vector()
        self.show_dot_product()
        self.show_same_angles()
        self.show_horizontal_bounce()
        self.let_process_play_out()

    def add_velocity_vectors(self):
        self.slide(1)
        self.ps_vect = self.get_ps_velocity_vector(self.trajectory)
        self.block_vectors = self.get_block_velocity_vectors(self.ps_vect)
        self.play(
            GrowArrow(self.ps_vect),
            LaggedStartMap(GrowArrow, self.block_vectors, run_time=1),
        )
        self.add(self.ps_vect, self.block_vectors)

    def contrast_d1_d2_line_with_xy_line(self):
        line = self.d1_eq_d2_line
        label = self.d1_eq_d2_label
        label.to_edge(RIGHT, buff=1.1)
        label.shift(0.65 * DOWN)

        xy_line = line.copy()
        xy_line.set_stroke(YELLOW, 3)
        xy_line.set_angle(45 * DEGREES)
        xy_label = TexMobject("x = y")
        xy_label.next_to(ORIGIN, DOWN, SMALL_BUFF)
        xy_label.rotate(45 * DEGREES, about_point=ORIGIN)
        xy_label.shift(xy_line.point_from_proportion(0.2))
        self.xy_group = VGroup(xy_line, xy_label)

        self.play(
            ShowPassingFlash(
                line.copy().set_stroke(YELLOW, 4)
            ),
            Write(label),
        )
        self.play(
            TransformFromCopy(line, xy_line, run_time=2)
        )
        self.play(Write(xy_label))
        self.wait()

    def rearrange_for_slope(self):
        eqs = VGroup(*reversed(self.axes.labels)).copy()
        y_eq, x_eq = eqs
        for eq in eqs:
            point = VectorizedPoint(eq[1].get_center())
            eq.submobjects.insert(1, point)
            eq.submobjects[3] = eq[3].submobjects[0]
            eq.generate_target()
        eqs_targets = VGroup(*[eq.target for eq in eqs])

        new_eqs = VGroup(
            TexMobject("{y", "\\over", "\\sqrt{m_2}}", "=", "d_2"),
            TexMobject("{x", "\\over", "\\sqrt{m_1}}", "=", "d_1"),
        )
        new_x_eq, new_y_eq = new_eqs
        # Shuffle to align with x_eq and y_eq
        for new_eq in new_eqs:
            new_eq[2][2:].set_color(BLUE)
            new_eq.submobjects = [new_eq[i] for i in [0, 1, 3, 2, 4]]

        eqs_targets.arrange(DOWN, buff=LARGE_BUFF)
        eqs_targets.move_to(RIGHT).to_edge(UP)
        for eq, new_eq in zip(eqs_targets, new_eqs):
            new_eq.move_to(eq)

        self.play(LaggedStartMap(MoveToTarget, eqs, lag_ratio=0.7))
        self.play(*[
            Transform(
                eq, new_eq,
                path_arc=-90 * DEGREES,
            )
            for eq, new_eq in zip(eqs, new_eqs)
        ])
        self.wait()

        # Shuffle back
        for eq in eqs:
            eq[2][2:].set_color(BLUE)
            eq.submobjects = [eq[i] for i in [0, 1, 3, 2, 4]]

        # Set equal
        equals = TexMobject("=")
        for eq in eqs:
            eq.generate_target()
        VGroup(
            x_eq.target[4],
            x_eq.target[3],
            x_eq.target[:3],
        ).arrange(RIGHT)
        for p1, p2 in zip(x_eq, x_eq.target):
            p2.align_to(p1, DOWN)
        group = VGroup(y_eq.target, equals, x_eq.target)
        group.arrange(RIGHT)
        x_eq.target.align_to(y_eq.target, DOWN)
        equals.align_to(y_eq.target[3], DOWN)
        group.to_edge(UP, buff=MED_SMALL_BUFF)
        group.to_edge(RIGHT, buff=3)

        self.play(
            MoveToTarget(y_eq),
            MoveToTarget(x_eq, path_arc=90 * DEGREES),
            GrowFromCenter(equals)
        )
        self.wait()

        # Simplify
        final_eq = TexMobject(
            "y", "=",
            "{\\sqrt{m_2}", "\\over", "\\sqrt{m_1}}",
            "x",
        )
        for part in final_eq.get_parts_by_tex("sqrt"):
            part[2:].set_color(BLUE)
        m_part = final_eq[2:5]

        final_eq.next_to(group, DOWN)
        final_eq.shift(0.4 * UP)
        movers = VGroup(
            y_eq[0], equals.submobjects[0],
            y_eq[2], y_eq[1], x_eq[2],
            x_eq[0]
        ).copy()
        for mover, part in zip(movers, final_eq):
            mover.target = part
        self.play(
            LaggedStartMap(
                MoveToTarget, movers,
                path_arc=30 * DEGREES,
                lag_ratio=0.9
            ),
            VGroup(x_eq, equals, y_eq).scale,
            0.7, {"about_edge": UP},
        )
        self.remove(movers)
        self.add(final_eq)
        self.wait()

        # Highlight slope
        flash_line = self.d1_eq_d2_line.copy()
        flash_line.set_stroke(YELLOW, 5)
        self.play(ShowPassingFlash(flash_line))
        self.play(ShowCreationThenFadeAround(m_part))
        self.wait()

        # Tuck away slope in mind
        slope = m_part.copy()
        slope.generate_target()
        randy = Randolph(height=1.5)
        randy.next_to(final_eq, LEFT, MED_SMALL_BUFF)
        randy.align_to(self.d2_eq_w2_line, DOWN)
        bubble = ThoughtBubble(
            height=1.3, width=1.3, direction=RIGHT,
        )
        bubble.pin_to(randy)
        slope.target.scale(0.5)
        slope.target.move_to(bubble.get_bubble_center())
        randy.change("pondering", slope.target)
        randy.save_state()
        randy.change("plane")
        randy.fade(1)

        self.play(
            Restore(randy),
            Write(bubble),
            MoveToTarget(slope)
        )
        self.play(Blink(randy))

        self.thinking_on_slope_group = VGroup(
            randy, bubble, slope,
        )

        self.to_fade = VGroup(
            eqs, equals, final_eq,
            self.thinking_on_slope_group,
            self.xy_group,
        )

    def up_to_first_collision(self):
        self.begin_sliding()
        self.play(FadeOut(self.to_fade))
        self.wait_until(
            lambda: abs(self.ps_velocity_vector.get_vector()[1]) > 0.01
        )
        self.end_sliding()
        self.wait(3 + 3 / 60)  # Final cut reasons

    def ask_what_next(self):
        ps_vect = self.ps_velocity_vector
        question = TextMobject("What next?")
        question.set_background_stroke(color=BLACK, width=3)
        question.next_to(self.ps_point, UP)

        self.play(FadeInFrom(question, DOWN))
        ps_vect.suspend_updating()
        angles = [0.75 * PI, -0.5 * PI, -0.25 * PI]
        for last_angle, angle in zip(np.cumsum([0] + angles), angles):
            # This is dumb and shouldn't be needed
            ps_vect.rotate(last_angle, about_point=ps_vect.get_start())
            target = ps_vect.copy()
            target.rotate(
                angle,
                about_point=ps_vect.get_start()
            )
            self.play(
                Transform(
                    ps_vect, target,
                    path_arc=angle
                ),
            )
        ps_vect.resume_updating()

        self.whats_next_question = question

    def show_conservation_of_momentum(self):
        equation = self.get_momentum_equation()

        # Main equation
        self.play(FadeInFromDown(equation))
        for part in equation[:2], equation[3:5]:
            outline = part.copy()
            outline.set_fill(opacity=0)
            outline.set_stroke(YELLOW, 3)
            self.play(ShowPassingFlash(
                outline,
                run_time=1.5
            ))
            self.wait(0.5)

        # Dot product
        dot_product = self.get_dot_product()
        dot_product.next_to(equation, DOWN)
        sqrty_m_array = dot_product[0]

        x_label, y_label = self.axes.labels

        self.play(
            FadeOut(self.whats_next_question),
            FadeIn(dot_product),
            Transform(
                x_label[2].copy(),
                sqrty_m_array.get_entries()[0],
                remover=True,
            ),
            Transform(
                y_label[2].copy(),
                sqrty_m_array.get_entries()[1],
                remover=True,
            ),
        )

        self.momentum_equation = equation
        self.dot_product = dot_product

    def show_rate_of_change_vector(self):
        ps_vect = self.ps_velocity_vector
        original_d_array = self.dot_product[2]

        d_array = original_d_array.copy()
        d_array.generate_target()
        d_array.scale(0.75)
        d_array.add_updater(lambda m: m.next_to(
            ps_vect.get_end(),
            np.sign(ps_vect.get_vector()[0]) * RIGHT,
            SMALL_BUFF
        ))

        self.play(TransformFromCopy(original_d_array, d_array))
        self.wait()

        self.d_array = d_array

    def show_sqrty_m_vector(self):
        original_sqrty_m_array = self.dot_product[0]
        sqrty_m_vector = Arrow(
            self.ds_to_point(0, 0),
            # self.ds_to_point(1, 1),
            self.ds_to_point(2, 2),
            buff=0,
            color=YELLOW,
        )

        sqrty_m_array = original_sqrty_m_array.deepcopy()
        sqrty_m_array.scale(0.75)
        sqrty_m_array.next_to(
            sqrty_m_vector.get_end(), UP, SMALL_BUFF
        )

        rise = DashedLine(
            sqrty_m_vector.get_end(),
            sqrty_m_vector.get_corner(DR),
            color=RED,
        )
        run = DashedLine(
            sqrty_m_vector.get_corner(DR),
            sqrty_m_vector.get_start(),
            color=GREEN,
        )
        sqrty_m_array.add_background_to_entries()
        run_label, rise_label = sqrty_m_array.get_entries().copy()
        rise_label.next_to(rise, RIGHT, SMALL_BUFF)
        run_label.next_to(run, DOWN, SMALL_BUFF)

        randy_group = self.thinking_on_slope_group
        randy_group.align_to(self.d2_eq_w2_line, DOWN)
        randy_group.to_edge(LEFT)
        randy_group.shift(2 * RIGHT)

        self.play(GrowArrow(sqrty_m_vector))
        self.play(TransformFromCopy(
            original_sqrty_m_array, sqrty_m_array,
        ))
        self.play(FadeIn(randy_group))
        self.play(
            ShowCreation(rise),
            TransformFromCopy(
                sqrty_m_array.get_entries()[1],
                rise_label,
            ),
        )
        self.add(run, randy_group)
        self.play(
            ShowCreation(run),
            TransformFromCopy(
                sqrty_m_array.get_entries()[0],
                run_label,
            ),
        )
        self.wait()
        self.play(FadeOut(randy_group))
        self.play(FadeOut(VGroup(
            rise, run, rise_label, run_label,
        )))

        # move to ps_point
        point = self.ps_point.get_location()
        sqrty_m_vector.generate_target()
        sqrty_m_vector.target.shift(
            point - sqrty_m_vector.get_start()
        )
        sqrty_m_array.generate_target()
        sqrty_m_array.target.next_to(
            sqrty_m_vector.target.get_end(),
            RIGHT, SMALL_BUFF,
        )
        sqrty_m_array.target.shift(SMALL_BUFF * UP)
        self.play(
            MoveToTarget(sqrty_m_vector),
            MoveToTarget(sqrty_m_array),
            run_time=2
        )

        self.sqrty_m_vector = sqrty_m_vector
        self.sqrty_m_array = sqrty_m_array

    def show_dot_product(self):
        # Highlight arrays
        d_array = self.d_array
        big_d_array = self.dot_product[2]
        m_array = self.sqrty_m_array
        big_m_array = self.dot_product[0]

        self.play(
            ShowCreationThenFadeAround(big_d_array),
            ShowCreationThenFadeAround(d_array),
        )
        self.play(
            ShowCreationThenFadeAround(big_m_array),
            ShowCreationThenFadeAround(m_array),
        )

        # Before and after
        ps_vect = self.ps_velocity_vector
        theta = np.arctan(np.sqrt(self.block2.mass / self.block1.mass))
        self.theta = theta

        ps_vect.suspend_updating()
        kwargs = {"about_point": ps_vect.get_start()}
        for x in range(2):
            for u in [-1, 1]:
                ps_vect.rotate(u * 2 * theta, **kwargs)
                self.update_mobjects(dt=0)
                self.wait()
        ps_vect.resume_updating()

        # Circle
        circle = Circle(
            radius=ps_vect.get_length(),
            arc_center=ps_vect.get_start(),
            color=RED,
            stroke_width=1,
        )
        self.play(
            Rotating(
                ps_vect,
                about_point=ps_vect.get_start(),
                run_time=5,
                rate_func=lambda t: smooth(t, 3),
            ),
            FadeIn(circle),
        )
        self.wait()

        self.ps_vect_circle = circle

    def show_same_angles(self):
        # circle = self.ps_vect_circle
        ps_vect = self.ps_velocity_vector
        ps_point = self.ps_point
        point = ps_point.get_center()
        ghost_ps_vect = ps_vect.copy()
        ghost_ps_vect.clear_updaters()
        ghost_ps_vect.set_fill(opacity=0.5)
        theta = self.theta
        ghost_ps_vect.rotate(
            -2 * theta,
            about_point=ghost_ps_vect.get_start(),
        )

        arc1 = Arc(
            start_angle=PI,
            angle=theta,
            arc_center=point,
            radius=0.5,
            color=WHITE,
        )
        arc2 = arc1.copy()
        arc2.rotate(theta, about_point=point)
        arc3 = arc1.copy()
        arc3.rotate(PI, about_point=point)
        arc1.set_color(BLUE)

        line_pair = VGroup(*[
            Line(point, point + LEFT).rotate(
                angle, about_point=point
            )
            for angle in [0, theta]
        ])
        line_pair.set_stroke(width=0)

        ps_vect.suspend_updating()
        self.play(
            ShowCreation(arc1),
            FadeIn(ghost_ps_vect)
        )
        self.wait()
        self.play(
            Rotate(ps_vect, 2 * theta, about_point=point)
        )
        self.begin_sliding()
        ps_vect.resume_updating()
        self.play(GrowFromPoint(arc2, point))
        self.wait(0.5)
        self.end_sliding()
        self.play(
            TransformFromCopy(arc1, arc3, path_arc=-PI),
            Rotate(line_pair, -PI, about_point=point),
            UpdateFromAlphaFunc(
                line_pair, lambda m, a: m.set_stroke(
                    width=there_and_back(a)**0.5
                )
            ),
        )
        # Show light beam along trajectory
        self.show_trajectory_beam_bounce()
        self.wait()

        # TODO: Add labels for angles?

        self.play(FadeOut(VGroup(
            self.ps_vect_circle, ghost_ps_vect, arc1
        )))

    def show_horizontal_bounce(self):
        self.slide_until(
            lambda: self.ps_velocity_vector.get_vector()[1] > 0
        )
        point = self.ps_point.get_location()
        theta = self.theta
        arc1 = Arc(
            start_angle=0,
            angle=2 * theta,
            radius=0.5,
            arc_center=point,
        )
        arc2 = arc1.copy()
        arc2.rotate(PI - 2 * theta, about_point=point)
        arcs = VGroup(arc1, arc2)

        self.slide(0.5)
        self.play(LaggedStartMap(
            FadeInFromLarge, arcs,
            lag_ratio=0.75,
        ))
        self.show_trajectory_beam_bounce()

    def let_process_play_out(self):
        self.slide(self.wait_time)
        self.wait(10)  # Just to be sure...

    #
    def show_trajectory_beam_bounce(self, n_times=2):
        # Show light beam along trajectory
        beam = self.trajectory.copy()
        beam.clear_updaters()
        beam.set_stroke(YELLOW, 3)
        for x in range(n_times):
            self.play(ShowPassingFlash(
                beam,
                run_time=2,
                rate_func=bezier([0, 0, 1, 1])
            ))

    def get_momentum_equation(self):
        equation = TexMobject(
            "m_1", "v_1", "+", "m_2", "v_2",
            "=", "\\text{const.}",
            tex_to_color_map={
                "m_1": BLUE,
                "m_2": BLUE,
                "v_1": RED,
                "v_2": RED,
            }
        )
        equation.to_edge(UP, buff=MED_SMALL_BUFF)
        return equation

    def get_dot_product(self,
                        m1="\\sqrt{m_1}", m2="\\sqrt{m_2}",
                        d1="dx/dt", d2="dy/dt"):
        sqrty_m = Matrix([[m1], [m2]])
        deriv_array = Matrix([[d1], [d2]])
        for entry in sqrty_m.get_entries():
            if "sqrt" in entry.get_tex_string():
                entry[2:].set_color(BLUE)
        for matrix in sqrty_m, deriv_array:
            matrix.add_to_back(BackgroundRectangle(matrix))
            matrix.get_brackets().scale(0.9)
            matrix.set_height(1.25)
        dot = TexMobject("\\cdot")
        rhs = TexMobject("= \\text{const.}")
        dot_product = VGroup(
            sqrty_m, dot, deriv_array, rhs
        )
        dot_product.arrange(RIGHT, buff=SMALL_BUFF)
        return dot_product


class JustTheProcessNew(PositionPhaseSpaceScene):
    CONFIG = {
        "block1_config": {
            "mass": 16,
            "velocity": -2
        },
        "wait_time": 10,
    }

    def setup(self):
        super().setup()
        self.add(
            self.floor,
            self.wall,
            self.blocks,
            self.axes,
            self.d1_eq_d2_line,
            self.d2_eq_w2_line,
        )

    def construct(self):
        self.slide(self.wait_time)
