from big_ol_pile_of_manim_imports import *
from functools import reduce

class Jewel(VMobject):
    CONFIG = {
        "color" : WHITE,
        "fill_opacity" : 0.75,
        "stroke_width" : 0,
        "height" : 0.5,
        "num_equator_points" : 5,
        "sun_vect" : OUT+LEFT+UP,
    }
    def generate_points(self):
        for vect in OUT, IN:
            compass_vects = list(compass_directions(self.num_equator_points))
            if vect is IN:
                compass_vects.reverse()
            for vect_pair in adjacent_pairs(compass_vects):
                self.add(Polygon(vect, *vect_pair))
        self.set_height(self.height)
        self.rotate(-np.pi/2-np.pi/24, RIGHT)        
        self.rotate(-np.pi/12, UP)
        self.submobjects.sort(
            key=lambda m: -m1.get_center()[2]
        )
        return self

class Necklace(VMobject):
    CONFIG = {
        "width" : FRAME_WIDTH - 1,
        "jewel_buff" : MED_SMALL_BUFF,
        "chain_color" : GREY,
        "default_colors" : [(4, BLUE), (6, WHITE), (4, GREEN)]
    }
    def __init__(self, *colors, **kwargs):
        digest_config(self, kwargs, locals())
        if len(colors) == 0:
            self.colors = self.get_default_colors()
        VMobject.__init__(self, **kwargs)

    def get_default_colors(self):
        result = list(it.chain(*[
            num*[color]
            for num, color in self.default_colors
        ]))
        random.shuffle(result)
        return result

    def generate_points(self):
        jewels = VGroup(*[
            Jewel(color = color)
            for color in self.colors
        ])
        jewels.arrange(buff = self.jewel_buff)
        jewels.set_width(self.width)
        jewels.center()
        j_to_j_dist = (jewels[1].get_center()-jewels[0].get_center())[0]

        chain = Line(
            jewels[0].get_center() + j_to_j_dist*LEFT/2, 
            jewels[-1].get_center() + j_to_j_dist*RIGHT/2, 
            color = self.chain_color,
        )
        self.add(chain, *jewels)
        self.chain = chain
        self.jewels = jewels

################

class FromPreviousTopologyVideo(Scene):
    def construct(self):
        rect = Rectangle(height = 9, width = 16)
        rect.set_height(FRAME_HEIGHT-2)
        title = TextMobject("From original ``Who cares about topology'' video")
        title.to_edge(UP)
        rect.next_to(title, DOWN)

        self.add(title)
        self.play(ShowCreation(rect))
        self.wait()

class CheckOutMathologer(PiCreatureScene):
    CONFIG = {
        "logo_height" : 1.5,
        "screen_height" : 5,
        "channel_name" : "Mathologer",
        "logo_file" : "mathologer_logo",
        "logo_color" : None,
    }
    def construct(self):
        logo = self.get_logo()
        name = TextMobject(self.channel_name)
        name.next_to(logo, RIGHT)

        rect = Rectangle(height = 9, width = 16)
        rect.set_height(self.screen_height)
        rect.next_to(logo, DOWN)
        rect.to_edge(LEFT)

        self.play(
            self.get_logo_intro_animation(logo),
            self.pi_creature.change_mode, "hooray",
        )
        self.play(
            ShowCreation(rect),
            Write(name)
        )
        self.wait(2)
        self.change_mode("happy")
        self.wait(2)

    def get_logo(self):
        logo = ImageMobject(self.logo_file)
        logo.set_height(self.logo_height)
        logo.to_corner(UP+LEFT)
        if self.logo_color is not None:
            logo.set_color(self.logo_color)
            logo.stroke_width = 1
        return logo

    def get_logo_intro_animation(self, logo):
        logo.save_state()
        logo.shift(DOWN)
        logo.set_color(BLACK)
        return ApplyMethod(logo.restore)

class IntroduceStolenNecklaceProblem(ThreeDScene):
    CONFIG = {
        "jewel_colors" : [BLUE, GREEN, WHITE, RED],
        "num_per_jewel" : [8, 10, 4, 6],
        "num_shuffles" : 1,
        "necklace_center" : UP,
        "random_seed" : 2,
        "forced_binary_choices" : (0, 1, 0, 1, 0),
    }
    def construct(self):
        random.seed(self.random_seed)
        self.add_thieves()
        self.write_title()
        self.introduce_necklace()
        self.divvy_by_cutting_all()
        self.divvy_with_n_cuts()
        self.shuffle_jewels(self.necklace.jewels)
        self.divvy_with_n_cuts()

    def add_thieves(self):
        thieves = VGroup(
            Randolph(),
            Mortimer()
        )
        thieves.arrange(RIGHT, buff = 4*LARGE_BUFF)
        thieves.to_edge(DOWN)
        thieves[0].make_eye_contact(thieves[1])

        self.add(thieves)
        self.thieves = thieves

    def write_title(self):
        title = TextMobject("Stolen necklace problem")
        title.to_edge(UP)
        self.play(
            Write(title),
            *[
                ApplyMethod(pi.look_at, title)
                for pi in self.thieves
            ]
        )
        self.title = title

    def introduce_necklace(self):
        necklace = self.get_necklace()
        jewels = necklace.jewels
        jewel_types = self.get_jewels_organized_by_type(jewels)

        enumeration_labels = VGroup()
        for jewel_type in jewel_types:
            num_mob = TexMobject(str(len(jewel_type)))
            jewel_copy = jewel_type[0].copy().scale(2)
            jewel_copy.next_to(num_mob)
            label = VGroup(num_mob, jewel_copy)
            enumeration_labels.add(label)
        enumeration_labels.arrange(RIGHT, buff = LARGE_BUFF)
        enumeration_labels.to_edge(UP)

        self.play(
            FadeIn(
                necklace,
                lag_ratio = 0.5,
                run_time = 3
            ),
            *it.chain(*[
                [pi.change_mode, "conniving", pi.look_at, necklace]
                for pi in self.thieves
            ])
        )
        self.play(*[
            ApplyMethod(
                jewel.rotate_in_place, np.pi/6, UP, 
                rate_func = there_and_back
            )
            for jewel in jewels
        ])
        self.play(Blink(self.thieves[0]))
        for jewel_type in jewel_types:
            self.play(
                jewel_type.shift, 0.2*UP,
                rate_func = wiggle
            )
        self.wait()
        for x in range(self.num_shuffles):
            self.shuffle_jewels(jewels)
        self.play(FadeOut(self.title))
        for jewel_type, label in zip(jewel_types, enumeration_labels):
            jewel_type.submobjects.sort(
                key=lambda m: m1.get
            )
            jewel_type.save_state()
            jewel_type.generate_target()
            jewel_type.target.arrange()
            jewel_type.target.scale(2)
            jewel_type.target.move_to(2*UP)
            self.play(
                MoveToTarget(jewel_type), 
                Write(label)
            )
            self.play(jewel_type.restore)
        self.play(Blink(self.thieves[1]))

        self.enumeration_labels = enumeration_labels
        self.jewel_types = jewel_types

    def divvy_by_cutting_all(self):
        enumeration_labels = self.enumeration_labels
        necklace = self.necklace
        jewel_types = self.jewel_types
        thieves = self.thieves

        both_half_labels = VGroup()
        for thief, vect in zip(self.thieves, [LEFT, RIGHT]):
            half_labels = VGroup()
            for label in enumeration_labels:
                tex, jewel = label
                num = int(tex.get_tex_string())
                half_label = VGroup(
                    TexMobject(str(num/2)),
                    jewel.copy()
                )
                half_label.arrange()
                half_labels.add(half_label)
            half_labels.arrange(DOWN)
            half_labels.set_height(thief.get_height())
            half_labels.next_to(
                thief, vect, 
                buff = MED_LARGE_BUFF,
                aligned_edge = DOWN
            )
            both_half_labels.add(half_labels)

        for half_labels in both_half_labels:
            self.play(ReplacementTransform(
                enumeration_labels.copy(), 
                half_labels
            ))
        self.play(*[ApplyMethod(pi.change_mode, "pondering") for pi in thieves])
        self.wait()

        for type_index, jewel_type in enumerate(jewel_types):
            jewel_type.save_state()
            jewel_type_copy = jewel_type.copy()
            n_jewels = len(jewel_type)
            halves = [
                VGroup(*jewel_type_copy[:n_jewels/2]),
                VGroup(*jewel_type_copy[n_jewels/2:]),
            ]
            for half, thief, vect in zip(halves, thieves, [RIGHT, LEFT]):
                half.arrange(DOWN)
                half.next_to(
                    thief, vect, 
                    buff = SMALL_BUFF + type_index*half.get_width(),
                    aligned_edge = DOWN
                )
            self.play(
                Transform(jewel_type, jewel_type_copy),
                *[
                    ApplyMethod(thief.look_at, jewel_type_copy)
                    for thief in thieves
                ]
            )
        self.play(*it.chain(*[
            [thief.change_mode, "happy", thief.look_at, necklace]
            for thief in thieves
        ]))
        self.wait()
        self.play(*[
            jewel_type.restore
            for jewel_type in jewel_types
        ])
        self.play(*it.chain(*[
            [thief.change_mode, "confused", thief.look_at, necklace]
            for thief in thieves
        ]))

    def divvy_with_n_cuts(
        self, 
        with_thieves = True, 
        highlight_groups = True,
        show_matching_after_divvying = True,
        ):
        necklace = self.necklace
        jewel_types = self.jewel_types
        jewels = sorted(
            necklace.jewels, 
            lambda m1, m2 : cmp(m1.get_center()[0], m2.get_center()[0])
        )
        slice_indices, binary_choices = self.find_slice_indices(jewels, jewel_types)
        subgroups = [
            VGroup(*jewels[i1:i2])
            for i1, i2 in zip(slice_indices, slice_indices[1:])
        ]
        buff = (jewels[1].get_left()[0]-jewels[0].get_right()[0])/2
        v_lines = VGroup(*[
            DashedLine(UP, DOWN).next_to(group, RIGHT, buff = buff)
            for group in subgroups[:-1]
        ])
        strand_groups = [VGroup(), VGroup()]
        for group, choice in zip(subgroups, binary_choices):
            strand = Line(
                group[0].get_center(), group[-1].get_center(),
                color = necklace.chain.get_color()
            )
            strand.add(*group)
            strand_groups[choice].add(strand)
            self.add(strand)

        self.play(ShowCreation(v_lines))
        self.play(
            FadeOut(necklace.chain),
            *it.chain(*[
                list(map(Animation, group))
                for group in strand_groups
            ])
        )
        for group in strand_groups:
            group.save_state()
        self.play(
            strand_groups[0].shift, UP/2.,
            strand_groups[1].shift, DOWN/2.,
        )
        if with_thieves:
            self.play(*it.chain(*[
                [thief.change_mode, "happy", thief.look_at, self.necklace]
                for thief in self.thieves
            ]))
            self.play(Blink(self.thieves[1]))
        else:
            self.wait()

        if highlight_groups:
            for group in strand_groups:
                box = Rectangle(
                    width = group.get_width()+2*SMALL_BUFF,
                    height = group.get_height()+2*SMALL_BUFF,
                    stroke_width = 0,
                    fill_color = YELLOW,
                    fill_opacity = 0.3,
                )
                box.move_to(group)
                self.play(FadeIn(box))
                self.wait()
                self.play(FadeOut(box))

        self.wait()
        if show_matching_after_divvying:
            for jewel_type in jewel_types:
                self.play(
                    *[
                        ApplyMethod(jewel.scale_in_place, 1.5)
                        for jewel in jewel_type
                    ],
                    rate_func = there_and_back,
                    run_time = 2
                )
            self.wait()
        self.play(
            FadeOut(v_lines),
            FadeIn(necklace.chain),
            *[
                group.restore for group in strand_groups
            ]
        )
        self.remove(*strand_groups)
        self.add(necklace)

    ########

    def get_necklace(self, **kwargs):
        colors = reduce(op.add, [
            num*[color]
            for num, color in zip(self.num_per_jewel, self.jewel_colors)
        ])
        self.necklace = Necklace(*colors, **kwargs)
        self.necklace.shift(self.necklace_center)
        return self.necklace

    def get_jewels_organized_by_type(self, jewels):
        return [
            VGroup(*[m for m in jewels if m.get_color() == color])
            for color in map(Color, self.jewel_colors)
        ]

    def shuffle_jewels(self, jewels, run_time = 2, path_arc = np.pi/2, **kwargs):
        shuffled_indices = list(range(len(jewels)))
        random.shuffle(shuffled_indices)
        target_group = VGroup(*[
            jewel.copy().move_to(jewels[shuffled_indices[i]])
            for i, jewel in enumerate(jewels)
        ])
        self.play(Transform(
            jewels, target_group,
            run_time = run_time,            
            path_arc = path_arc,
            **kwargs
        ))

    def find_slice_indices(self, jewels, jewel_types):

        def jewel_to_type_number(jewel):
            for i, jewel_type in enumerate(jewel_types):
                if jewel in jewel_type:
                    return i
            raise Exception("Not in any jewel_types")
        type_numbers = list(map(jewel_to_type_number, jewels))

        n_types = len(jewel_types)
        for slice_indices in it.combinations(list(range(1, len(jewels))), n_types):
            slice_indices = [0] + list(slice_indices) + [len(jewels)]
            if self.forced_binary_choices is not None:
                all_binary_choices = [self.forced_binary_choices]
            else:
                all_binary_choices = it.product(*[list(range(2))]*(n_types+1))
            for binary_choices in all_binary_choices:
                subsets = [
                    type_numbers[i1:i2]
                    for i1, i2 in zip(slice_indices, slice_indices[1:])
                ]
                left_sets, right_sets = [
                    [
                        subset
                        for subset, index in zip(subsets, binary_choices)
                        if index == target_index
                    ]
                    for target_index in range(2)
                ]
                flat_left_set = np.array(list(it.chain(*left_sets)))
                flat_right_set = np.array(list(it.chain(*right_sets)))


                match_array = [
                    sum(flat_left_set == n) == sum(flat_right_set == n)
                    for n in range(n_types)
                ]
                if np.all(match_array):
                    return slice_indices, binary_choices
        raise Exception("No fair division found")

class ThingToProve(PiCreatureScene):
    def construct(self):
        arrow = Arrow(UP, DOWN)
        top_words = TextMobject("$n$ jewel types")
        top_words.next_to(arrow, UP)
        bottom_words = TextMobject("""
            Fair division possible
            with $n$ cuts
        """)
        bottom_words.next_to(arrow, DOWN)

        self.play(
            Write(top_words, run_time = 2),
            self.pi_creature.change_mode, "raise_right_hand"
        )
        self.play(ShowCreation(arrow))
        self.play(
            Write(bottom_words, run_time = 2),
            self.pi_creature.change_mode, "pondering"
        )
        self.wait(3)

class FiveJewelCase(IntroduceStolenNecklaceProblem):
    CONFIG = {
        "jewel_colors" : [BLUE, GREEN, WHITE, RED, YELLOW],
        "num_per_jewel" : [6, 4, 4, 2, 8],
        "forced_binary_choices" : (0, 1, 0, 1, 0, 1),
    }
    def construct(self):
        random.seed(self.random_seed)
        self.add(self.get_necklace())
        jewels = self.necklace.jewels
        self.shuffle_jewels(jewels, run_time = 0)
        self.jewel_types = self.get_jewels_organized_by_type(jewels)     
        self.add_title()
        self.add_thieves()
        for thief in self.thieves:
            ApplyMethod(thief.change_mode, "pondering").update(1)
            thief.look_at(self.necklace)
        self.divvy_with_n_cuts()

    def add_title(self):
        n_cuts = len(self.jewel_colors)
        title = TextMobject(
            "%d jewel types, %d cuts"%(n_cuts, n_cuts)
        )
        title.to_edge(UP)
        self.add(title)

class SixJewelCase(FiveJewelCase):
    CONFIG = {
        "jewel_colors" : [BLUE, GREEN, WHITE, RED, YELLOW, MAROON_B],
        "num_per_jewel" : [6, 4, 4, 2, 2, 6],
        "forced_binary_choices" : (0, 1, 0, 1, 0, 1, 0),
    }

class DiscussApplicability(TeacherStudentsScene):
    def construct(self):
        self.teacher_says("""
            Minimize sharding,
            allocate resources evenly
        """)
        self.change_student_modes(*["pondering"]*3)
        self.wait(2)

class ThreeJewelCase(FiveJewelCase):
    CONFIG = {
        "jewel_colors" : [BLUE, GREEN, WHITE],
        "num_per_jewel" : [6, 4, 8],
        "forced_binary_choices" : (0, 1, 0, 1),
    }

class RepeatedShuffling(IntroduceStolenNecklaceProblem):
    CONFIG = {
        "num_shuffles" : 5,
        "random_seed" : 3,
        "show_matching_after_divvying" : False,
    }
    def construct(self):
        random.seed(self.random_seed)
        self.add(self.get_necklace())
        jewels = self.necklace.jewels
        self.jewel_types = self.get_jewels_organized_by_type(jewels)     
        self.add_thieves()
        for thief in self.thieves:
            ApplyMethod(thief.change_mode, "pondering").update(1)
            thief.look_at(self.necklace)

        for x in range(self.num_shuffles):
            self.shuffle_jewels(jewels)
            self.divvy_with_n_cuts(
                show_matching_after_divvying = False
            )

class NowForTheTopology(TeacherStudentsScene):
    def construct(self):
        self.teacher_says("Now for the \\\\ topology")
        self.change_student_modes(*["hooray"]*3)
        self.wait(3)

class ExternallyAnimatedScene(Scene):
    def construct(self):
        raise Exception("Don't actually run this class.")

class SphereOntoPlaneIn3D(ExternallyAnimatedScene):
    pass

class DiscontinuousSphereOntoPlaneIn3D(ExternallyAnimatedScene):
    pass

class WriteWords(Scene):
    CONFIG = {
        "words" : "",
        "color" : WHITE,
    }
    def construct(self):
        words = TextMobject(self.words)
        words.set_color(self.color)
        words.set_width(FRAME_WIDTH-1)
        words.to_edge(DOWN)
        self.play(Write(words))
        self.wait(2)

class WriteNotAllowed(WriteWords):
    CONFIG = {
        "words" : "Not allowed",
        "color" : RED,
    }

class NonAntipodalCollisionIn3D(ExternallyAnimatedScene):
    pass

class AntipodalCollisionIn3D(ExternallyAnimatedScene):
    pass

class WriteBorsukUlam(WriteWords):
    CONFIG = {
        "words" : "Borsuk-Ulam Theorem",
    }

class WriteAntipodal(WriteWords):
    CONFIG = {
        "words" : "``Antipodal''",
        "color" : MAROON_B,
    }

class ProjectOntoEquatorIn3D(ExternallyAnimatedScene):
    pass

class ProjectOntoEquatorWithPolesIn3D(ExternallyAnimatedScene):
    pass

class ProjectAntipodalNonCollisionIn3D(ExternallyAnimatedScene):
    pass

class ShearThenProjectnOntoEquatorPolesMissIn3D(ExternallyAnimatedScene):
    pass

class ShearThenProjectnOntoEquatorAntipodalCollisionIn3D(ExternallyAnimatedScene):
    pass

class ClassicExample(TeacherStudentsScene):
    def construct(self):
        self.teacher_says("The classic example...")
        self.change_student_modes(*["happy"]*3)
        self.wait(2)

class AntipodalEarthPoints(ExternallyAnimatedScene):
    pass

class RotatingEarth(ExternallyAnimatedScene):
    pass

class TemperaturePressurePlane(GraphScene):
    CONFIG = {
        "x_labeled_nums" : [],
        "y_labeled_nums" : [],
        "x_axis_label" : "Temperature",
        "y_axis_label" : "Pressure",
        "graph_origin" : 2.5*DOWN + 2*LEFT,
        "corner_square_width" : 4,
        "example_point_coords" : (2, 5),
    }
    def construct(self):
        self.setup_axes()
        self.draw_arrow()
        self.add_example_coordinates()
        self.wander_continuously()

    def draw_arrow(self):
        square = Square(
            side_length = self.corner_square_width,
            stroke_color = WHITE,
            stroke_width = 0,
        )
        square.to_corner(UP+LEFT, buff = 0)

        arrow = Arrow(
            square.get_right(), 
            self.coords_to_point(*self.example_point_coords)
        )

        self.play(ShowCreation(arrow))

    def add_example_coordinates(self):
        dot = Dot(self.coords_to_point(*self.example_point_coords))
        dot.set_color(YELLOW)
        tex = TexMobject("(25^\\circ\\text{C}, 101 \\text{ kPa})")
        tex.next_to(dot, UP+RIGHT, buff = SMALL_BUFF)

        self.play(ShowCreation(dot))
        self.play(Write(tex))
        self.wait()
        self.play(FadeOut(tex))

    def wander_continuously(self):
        path = VMobject().set_points_smoothly([
            ORIGIN, 2*UP+RIGHT, 2*DOWN+RIGHT,
            5*RIGHT, 4*RIGHT+UP, 3*RIGHT+2*DOWN,
            DOWN+LEFT, 2*RIGHT
        ])
        point = self.coords_to_point(*self.example_point_coords)
        path.shift(point)

        path.set_color(GREEN)

        self.play(ShowCreation(path, run_time = 10, rate_func=linear))
        self.wait()

class AlternateSphereSquishing(ExternallyAnimatedScene):
    pass

class AlternateAntipodalCollision(ExternallyAnimatedScene):
    pass

class AskWhy(TeacherStudentsScene):
    def construct(self):
        self.student_says("But...why?")
        self.change_student_modes("pondering", None, "thinking")
        self.play(self.get_teacher().change_mode, "happy")
        self.wait(3)

class PointOutVSauce(CheckOutMathologer):
    CONFIG = {
        "channel_name" : "",
        "logo_file" : "Vsauce_logo",
        "logo_height" : 1,
        "logo_color" : GREY,
    }
    def get_logo(self):
        logo = SVGMobject(file_name = self.logo_file)
        logo.set_height(self.logo_height)
        logo.to_corner(UP+LEFT)
        logo.set_stroke(width = 0)
        logo.set_fill(GREEN)
        logo.sort()
        return logo

    def get_logo_intro_animation(self, logo):
        return DrawBorderThenFill(
            logo,
            run_time = 2,
        )

class WalkEquatorPostTransform(GraphScene):
    CONFIG = {
        "x_labeled_nums" : [],
        "y_labeled_nums" : [],
        "graph_origin" : 2.5*DOWN + 2*LEFT,
        "curved_arrow_color" : WHITE,
        "curved_arrow_radius" : 3,
        "num_great_arcs" : 10,
    }
    def construct(self):
        self.setup_axes()
        self.add_curved_arrow()
        self.great_arc_images = self.get_great_arc_images()

        self.walk_equator()
        self.walk_tilted_equator()
        self.draw_transverse_curve()
        self.walk_transverse_curve()

    def add_curved_arrow(self):
        arc = Arc(
            start_angle = 2*np.pi/3, angle = -np.pi/2, 
            radius = self.curved_arrow_radius,
            color = self.curved_arrow_color
        )
        arc.add_tip()
        arc.move_to(self.coords_to_point(0, 7))

        self.add(arc)

    def walk_equator(self):
        equator = self.great_arc_images[0]
        dots = VGroup(Dot(), Dot())
        dots.set_color(MAROON_B)
        dot_movement = self.get_arc_walk_dot_movement(equator, dots)
        dot_movement.update(0)

        self.play(ShowCreation(equator, run_time = 3))
        self.play(FadeIn(dots[0]))
        dots[1].set_fill(opacity = 0)
        self.play(dot_movement)
        self.play(dots[1].set_fill, None, 1)
        self.play(dot_movement)
        self.play(dot_movement)

        proportion = equator.collision_point_proportion
        self.play(self.get_arc_walk_dot_movement(
            equator, dots,
            rate_func = lambda t : 2*proportion*smooth(t)
        ))
        v_line = DashedLine(FRAME_Y_RADIUS*UP, FRAME_Y_RADIUS*DOWN)
        v_line.shift(dots.get_center()[0]*RIGHT)
        self.play(ShowCreation(v_line))
        self.wait()
        self.play(FadeOut(v_line))

        dots.save_state()
        equator.save_state()
        self.play(
            equator.fade,
            dots.fade
        )

        self.first_dots = dots

    def walk_tilted_equator(self):
        equator = self.great_arc_images[0]
        tilted_eq = self.great_arc_images[1]

        dots = VGroup(Dot(), Dot())
        dots.set_color(MAROON_B)
        dot_movement = self.get_arc_walk_dot_movement(tilted_eq, dots)
        dot_movement.update(0)

        self.play(ReplacementTransform(equator.copy(), tilted_eq))
        self.wait()
        self.play(FadeIn(dots))
        self.play(dot_movement)

        proportion = tilted_eq.collision_point_proportion
        self.play(self.get_arc_walk_dot_movement(
            tilted_eq, dots,
            rate_func = lambda t : 2*proportion*smooth(t)
        ))
        v_line = DashedLine(FRAME_Y_RADIUS*UP, FRAME_Y_RADIUS*DOWN)
        v_line.shift(dots.get_center()[0]*RIGHT)
        self.play(ShowCreation(v_line))
        self.wait()
        self.play(FadeOut(v_line))
        self.play(*list(map(FadeOut, [tilted_eq, dots])))

    def draw_transverse_curve(self):
        transverse_curve = self.get_transverse_curve(self.great_arc_images)
        dots = self.first_dots
        equator = self.great_arc_images[0]

        self.play(dots.restore)
        equator.restore()
        self.great_arc_images.fade()

        target_arcs = list(self.great_arc_images[1:])
        target_dots = []
        for arc in target_arcs:
            new_dots = dots.copy()
            for dot, point in zip(new_dots, arc.x_collision_points):
                dot.move_to(point)
            target_dots.append(new_dots)

        alt_eq = equator.copy()
        alt_eq.points = np.array(list(reversed(alt_eq.points)))
        alt_dots = dots.copy()
        alt_dots.submobjects.reverse()
        target_arcs += [alt_eq, alt_eq.copy()]
        target_dots += [alt_dots, alt_dots.copy()]

        equator_transform = Succession(*[
            Transform(equator, arc, rate_func=linear)
            for arc in target_arcs
        ])
        dots_transform = Succession(*[
            Transform(dots, target, rate_func=linear)
            for target in target_dots
        ])

        self.play(
            ShowCreation(transverse_curve, lag_ratio = 0),
            equator_transform,
            dots_transform,
            run_time = 10,
            rate_func=linear,
        )
        self.wait(2)

    def walk_transverse_curve(self):
        transverse_curve = self.get_transverse_curve(self.great_arc_images)
        dots = self.first_dots

        def dot_update(dots, alpha):
            for dot, curve in zip(dots, transverse_curve):
                dot.move_to(curve.point_from_proportion(alpha))
            return dots

        for x in range(2):
            self.play(
                UpdateFromAlphaFunc(dots, dot_update),
                run_time = 4
            )
        self.play(
            UpdateFromAlphaFunc(dots, dot_update),
            run_time = 4,
            rate_func = lambda t : 0.455*smooth(t)
        )
        self.play(
            dots.set_color, YELLOW,
            dots.scale_in_place, 1.2,
            rate_func = there_and_back
        )
        self.wait()

    #######

    def get_arc_walk_dot_movement(self, arc, dots, **kwargs):
        def dot_update(dots, alpha):
            dots[0].move_to(arc.point_from_proportion(0.5*alpha))
            dots[1].move_to(arc.point_from_proportion(0.5+0.5*alpha))
            return dots
        if "run_time" not in kwargs:
            kwargs["run_time"] = 5
        return UpdateFromAlphaFunc(dots, dot_update, **kwargs)

    def sphere_to_plane(self, point):
        x, y, z = point
        return np.array([
            x - 2*x*z + y + 1,
            y+0.5*y*np.cos(z*np.pi),
            0
        ])

    def sphere_point(self, portion_around_equator, equator_tilt = 0):
        theta = portion_around_equator*2*np.pi
        point = np.cos(theta)*RIGHT + np.sin(theta)*UP
        phi = equator_tilt*np.pi
        return rotate_vector(point, phi, RIGHT)

    def get_great_arc_images(self):
        curves = VGroup(*[
            ParametricFunction(
                lambda t : self.sphere_point(t, s)
            ).apply_function(self.sphere_to_plane)
            for s in np.arange(0, 1, 1./self.num_great_arcs)
            # for s in [0]
        ])
        curves.set_color(YELLOW)
        curves[0].set_color(RED)
        for curve in curves:
            antipodal_x_diff = lambda x : \
                curve.point_from_proportion(x+0.5)[0]-\
                curve.point_from_proportion(x)[0]
            last_x = 0                
            last_sign = np.sign(antipodal_x_diff(last_x))
            for x in np.linspace(0, 0.5, 100):
                sign = np.sign(antipodal_x_diff(x))
                if sign != last_sign:
                    mean = np.mean([last_x, x])
                    curve.x_collision_points = [
                        curve.point_from_proportion(mean),
                        curve.point_from_proportion(mean+0.5),
                    ]
                    curve.collision_point_proportion = mean
                    break
                last_x = x
                last_sign = sign
        return curves

    def get_transverse_curve(self, gerat_arc_images):
        points = list(it.chain(*[
            [
                curve.x_collision_points[i]
                for curve in gerat_arc_images
            ]
            for i in (0, 1)
        ]))
        full_curve = VMobject(close_new_points = True)
        full_curve.set_points_smoothly(points + [points[0]])
        full_curve.set_color(MAROON_B)
        first_half = full_curve.copy().pointwise_become_partial(
            full_curve, 0, 0.5
        )
        second_half = first_half.copy().rotate_in_place(np.pi, RIGHT)
        broken_curve = VGroup(first_half, second_half)
        return broken_curve

class WalkAroundEquatorPreimage(ExternallyAnimatedScene):
    pass

class WalkTiltedEquatorPreimage(ExternallyAnimatedScene):
    pass

class FormLoopTransverseToEquator(ExternallyAnimatedScene):
    pass

class AntipodalWalkAroundTransverseLoop(ExternallyAnimatedScene):
    pass

class MentionGenerality(TeacherStudentsScene, ThreeDScene):
    def construct(self):
        necklace = Necklace(width = FRAME_X_RADIUS)
        necklace.shift(2*UP)
        necklace.to_edge(RIGHT)
        arrow = TexMobject("\\Leftrightarrow")
        arrow.scale(2)
        arrow.next_to(necklace, LEFT)
        q_marks = TexMobject("???")
        q_marks.next_to(arrow, UP)
        arrow.add(q_marks)

        formula = TexMobject("f(\\textbf{x}) = f(-\\textbf{x})")
        formula.next_to(self.get_students(), UP, buff = LARGE_BUFF)
        formula.to_edge(LEFT, buff = LARGE_BUFF)

        self.play(
            self.teacher.change_mode, "raise_right_hand",
            self.teacher.look_at, arrow
        )
        self.play(
            FadeIn(necklace, run_time = 2, lag_ratio = 0.5),
            Write(arrow),
            *[
                ApplyMethod(pi.look_at, arrow)
                for pi in self.get_pi_creatures()
            ]
        )
        self.change_student_modes("pondering", "erm", "confused")
        self.wait()
        self.play(*[
            ApplyMethod(pi.look_at, arrow)
            for pi in self.get_pi_creatures()
        ])
        self.play(Write(formula))
        self.wait(3)

class SimpleSphere(ExternallyAnimatedScene):
    pass

class PointsIn3D(Scene):
    CONFIG = {
        # "colors" : [RED, GREEN, BLUE],
        "colors" : color_gradient([GREEN, BLUE], 3),
    }
    def construct(self):
        sphere_def = TextMobject(
            "\\doublespacing Sphere in 3D: All", "$(x_1, x_2, x_3)$\\\\", 
            "such that", "$x_1^2 + x_2^2 + x_3^2 = 1$",
            alignment = "",
        )
        sphere_def.next_to(ORIGIN, DOWN)
        for index, subindex_list in (1, [1, 2, 4, 5, 7, 8]), (3, [0, 2, 4, 6, 8, 10]):
            colors = np.repeat(self.colors, 2)
            for subindex, color in zip(subindex_list, colors):
                sphere_def[index][subindex].set_color(color)

        point_ex = TextMobject(
            "For example, ", 
            "(", "0.41", ", ", "-0.58", ", ", "0.71", ")",
            arg_separator = ""
        )
        for index, color in zip([2, 4, 6], self.colors):
            point_ex[index].set_color(color)
        point_ex.scale(0.8)
        point_ex.next_to(
            sphere_def[1], UP+RIGHT,
            buff = 1.5*LARGE_BUFF
        )
        point_ex.shift_onto_screen()
        arrow = Arrow(sphere_def[1].get_top(), point_ex.get_bottom())

        self.play(Write(sphere_def[1]))
        self.play(ShowCreation(arrow))
        self.play(Write(point_ex))
        self.wait()
        self.play(
            Animation(sphere_def[1].copy(), remover = True),
            Write(sphere_def),
        )
        self.wait()

class AntipodalPairToBeGivenCoordinates(ExternallyAnimatedScene):
    pass

class WritePointCoordinates(Scene):
    CONFIG = {
        "colors" : color_gradient([GREEN, BLUE], 3),
        "corner" : DOWN+RIGHT,
    }
    def construct(self):
        coords = self.get_coords()
        arrow = Arrow(
            -self.corner, self.corner, 
            stroke_width = 8,
            color = MAROON_B
        )
        x_component = self.corner[0]*RIGHT
        y_component = self.corner[1]*UP
        arrow.next_to(
            coords.get_edge_center(y_component), 
            y_component, 
            aligned_edge = -x_component,
            buff = MED_SMALL_BUFF
        )

        group = VGroup(coords, arrow)
        group.scale(2)        
        group.to_corner(self.corner)


        self.play(FadeIn(coords))
        self.play(ShowCreation(arrow))
        self.wait()

    def get_coords(self):
        coords = TexMobject(
            "(", "0.41", ", ", "-0.58", ", ", "0.71", ")",
            arg_separator = ""
        )
        for index, color in zip([1, 3, 5], self.colors):
            coords[index].set_color(color)
        return coords

class WriteAntipodalCoordinates(WritePointCoordinates):
    CONFIG = {
        "corner" : UP+LEFT,
        "sign_color" : RED,
    }

    def get_coords(self):
        coords = TexMobject(
            "(", "-", "0.41", ", ", "+", "0.58", ", ", "-", "0.71", ")",
            arg_separator = ""
        )
        for index, color in zip([2, 5, 8], self.colors):
            coords[index].set_color(color)
            coords[index-1].set_color(self.sign_color)
        return coords

class GeneralizeBorsukUlam(Scene):
    CONFIG = {
        "n_dims" : 3,
        "boundary_colors" : [GREEN_B, BLUE],
        "output_boundary_color" : [MAROON_B, YELLOW],
        "negative_color" : RED,
    }
    def setup(self):
        self.colors = color_gradient(self.boundary_colors, self.n_dims)

    def construct(self):
        sphere_set = self.get_sphere_set()
        arrow = Arrow(LEFT, RIGHT)
        f = TexMobject("f")
        output_space = self.get_output_space()
        equation = self.get_equation()

        sphere_set.to_corner(UP+LEFT)
        arrow.next_to(sphere_set, RIGHT)
        f.next_to(arrow, UP)
        output_space.next_to(arrow, RIGHT)
        equation.next_to(sphere_set, DOWN, buff = LARGE_BUFF)
        equation.to_edge(RIGHT)
        lhs = VGroup(*equation[:2])
        eq = equation[2]
        rhs = VGroup(*equation[3:])

        self.play(FadeIn(sphere_set))
        self.wait()
        self.play(
            ShowCreation(arrow),
            Write(f)
        )
        self.play(Write(output_space))
        self.wait()
        self.play(FadeIn(lhs))
        self.play(
            ReplacementTransform(lhs.copy(), rhs),
            Write(eq)
        )
        self.wait()

    def get_condition(self):
        squares = list(map(TexMobject, [
            "x_%d^2"%d
            for d in range(1, 1+self.n_dims)
        ]))
        for square, color in zip(squares, self.colors):
            square[0].set_color(color)
            square[-1].set_color(color)
        plusses = [TexMobject("+") for x in range(self.n_dims-1)]
        plusses += [TexMobject("=1")]
        condition = VGroup(*it.chain(*list(zip(squares, plusses))))
        condition.arrange(RIGHT)

        return condition

    def get_tuple(self):
        mid_parts = list(it.chain(*[
            ["x_%d"%d, ","]
            for d in range(1, self.n_dims)
        ]))
        tup = TexMobject(*["("] + mid_parts + ["x_%d"%self.n_dims, ")"])
        for index, color in zip(it.count(1, 2), self.colors):
            tup[index].set_color(color)

        return tup

    def get_negative_tuple(self):
        mid_parts = list(it.chain(*[
            ["-", "x_%d"%d, ","]
            for d in range(1, self.n_dims)
        ]))
        tup = TexMobject(*["("] + mid_parts + ["-", "x_%d"%self.n_dims, ")"])
        for index, color in zip(it.count(1, 3), self.colors):
            tup[index].set_color(self.negative_color)
            tup[index+1].set_color(color)

        return tup

    def get_output_space(self):
        return TextMobject("%dD space"%(self.n_dims-1))
        # n_dims = self.n_dims-1
        # colors = color_gradient(self.output_boundary_color, n_dims)
        # mid_parts = list(it.chain(*[
        #     ["y_%d"%d, ","]
        #     for d in range(1, n_dims)
        # ]))
        # tup = TexMobject(*["("] + mid_parts + ["y_%d"%n_dims, ")"])
        # for index, color in zip(it.count(1, 2), colors):
        #     tup[index].set_color(color)

        # return tup

    def get_equation(self):
        tup = self.get_tuple()
        neg_tup = self.get_negative_tuple()
        f1, f2 = [TexMobject("f") for x in range(2)]
        equals = TexMobject("=")
        equation = VGroup(f1, tup, equals, f2, neg_tup)
        equation.arrange(RIGHT, buff = SMALL_BUFF)

        return equation

    def get_sphere_set(self):
        tup = self.get_tuple()
        such_that = TextMobject("such that")
        such_that.next_to(tup, RIGHT)
        condition = self.get_condition()
        condition.next_to(
            tup, DOWN, 
            buff = MED_LARGE_BUFF,
            aligned_edge = LEFT
        )
        group = VGroup(tup, such_that, condition)
        l_brace = Brace(group, LEFT)
        r_brace = Brace(group, RIGHT)
        group.add(l_brace, r_brace)

        return group

# class FiveDBorsukUlam(GeneralizeBorsukUlam):
#     CONFIG = {
#         "n_dims" : 5,
#     }

class MentionMakingNecklaceProblemContinuous(TeacherStudentsScene):
    def construct(self):
        self.teacher_says("""
            Translate this into
            a continuous problem.
        """)
        self.change_student_modes("confused", "pondering", "erm")
        self.wait(3)

class MakeTwoJewelCaseContinuous(IntroduceStolenNecklaceProblem):
    CONFIG = {
        "jewel_colors" : [BLUE, GREEN],
        "num_per_jewel" : [8, 10],
        "random_seed" : 2,
        "forced_binary_choices" : (0, 1, 0),
        "show_matching_after_divvying" : True,
        "necklace_center" : ORIGIN,
        "necklace_width" : FRAME_WIDTH - 3,
        "random_seed" : 0,
        "num_continuous_division_searches" : 4,
    }
    def construct(self):
        random.seed(self.random_seed)
        self.introduce_necklace()
        self.divvy_with_n_cuts()
        self.identify_necklace_with_unit_interval()
        self.color_necklace()
        self.find_continuous_fair_division()
        self.show_continuous_fair_division()
        self.set_color_continuous_groups()
        self.mention_equivalence_to_discrete_case()
        self.shift_divide_off_tick_marks()

    def introduce_necklace(self):
        self.get_necklace(
            width = self.necklace_width,
        )
        self.play(FadeIn(
            self.necklace,
            lag_ratio = 0.5
        ))
        self.shuffle_jewels(self.necklace.jewels)
        jewel_types = self.get_jewels_organized_by_type(
            self.necklace.jewels
        )
        self.wait()
        self.count_jewel_types(jewel_types)
        self.wait()

        self.jewel_types = jewel_types

    def count_jewel_types(self, jewel_types):
        enumeration_labels = VGroup()
        for jewel_type in jewel_types:
            num_mob = TexMobject(str(len(jewel_type)))
            jewel_copy = jewel_type[0].copy()
            # jewel_copy.set_height(num_mob.get_height())
            jewel_copy.next_to(num_mob)
            label = VGroup(num_mob, jewel_copy)
            enumeration_labels.add(label)
        enumeration_labels.arrange(RIGHT, buff = LARGE_BUFF)
        enumeration_labels.to_edge(UP)

        for jewel_type, label in zip(jewel_types, enumeration_labels):
            jewel_type.sort_submobjects()
         
            jewel_type.save_state()
            jewel_type.generate_target()
            jewel_type.target.arrange()
            jewel_type.target.move_to(2*UP)
            self.play(
                MoveToTarget(jewel_type), 
                Write(label)
            )
            self.play(jewel_type.restore)

    def divvy_with_n_cuts(self):
        IntroduceStolenNecklaceProblem.divvy_with_n_cuts(
            self, 
            with_thieves = False, 
            highlight_groups = False,
            show_matching_after_divvying = True,
        )

    def identify_necklace_with_unit_interval(self):
        interval = UnitInterval(
            tick_frequency = 1./sum(self.num_per_jewel),
            tick_size = 0.2,
            numbers_with_elongated_ticks = [],
        )
        interval.stretch_to_fit_width(self.necklace.get_width())
        interval.move_to(self.necklace)
        tick_marks = interval.tick_marks
        tick_marks.set_stroke(WHITE, width = 2)

        brace = Brace(interval)
        brace_text = brace.get_text("Length = 1")

        self.play(
            GrowFromCenter(brace),
            Write(brace_text)
        )
        self.wait()
        self.play(
            ShowCreation(interval.tick_marks),
        )
        self.wait()

        self.tick_marks = interval.tick_marks
        self.length_brace = VGroup(brace, brace_text)

    def color_necklace(self):
        example_index = len(self.necklace.jewels)/2
        jewels = self.necklace.jewels
        chain = self.necklace.chain
        self.remove(self.necklace)
        self.add(chain, jewels)

        jewels.submobjects.sort(
            key=lambda m: m.get_center()[0]
        )
        remaining_indices = list(range(len(jewels)))
        remaining_indices.remove(example_index)

        example_segment = self.color_necklace_by_indices(example_index)
        remaining_segments = self.color_necklace_by_indices(*remaining_indices)
        self.remove(chain)
        segments = VGroup(example_segment[0], *remaining_segments)
        segments.submobjects.sort(
            key=lambda m: m.get_center()[0]
        )
        segment_types = VGroup(*[
            VGroup(*[m for m in segments if m.get_color() == Color(color)])
            for color in self.jewel_colors
        ])

        for group in segment_types:
            length_tex = TexMobject("\\frac{%d}{%d}"%(
                len(group),
                len(jewels)
             ))
            length_tex.next_to(group, UP)
            length_tex.shift(UP)
            self.play(
                group.shift, UP,
                Write(length_tex, run_time = 1),
            )
            self.wait()
            self.play(
                group.shift, DOWN,
                FadeOut(length_tex)
            )
        self.play(FadeOut(self.length_brace))

        self.segments = segments

    def color_necklace_by_indices(self, *indices):
        chain = self.necklace.chain
        jewels = VGroup(*[
            self.necklace.jewels[i]
            for i in indices
        ])
        n_jewels = len(self.necklace.jewels)

        segments = VGroup(*[
            Line(
                chain.point_from_proportion(index/float(n_jewels)),
                chain.point_from_proportion((index+1)/float(n_jewels)),
                color = jewel.get_color()
            )
            for index, jewel in zip(indices, jewels)
        ])
        for jewel in jewels:
            jewel.save_state()

        self.play(jewels.shift, jewels.get_height()*UP)
        self.play(ReplacementTransform(
            jewels, segments,
            lag_ratio = 0.5,
            run_time = 2
        ))
        self.wait()
        return segments

    def find_continuous_fair_division(self):
        chain = self.necklace.chain
        n_jewels = len(self.necklace.jewels)

        slice_indices, ignore = self.find_slice_indices(
            self.necklace.jewels,
            self.jewel_types
        )
        cut_proportions = [
            sorted([random.random(), random.random()])
            for x in range(self.num_continuous_division_searches)
        ]
        cut_proportions.append([
            float(i)/n_jewels
            for i in slice_indices[1:-1]
        ])
        cut_points = [
            list(map(chain.point_from_proportion, pair))
            for pair in cut_proportions
        ]
        v_lines = VGroup(*[DashedLine(UP, DOWN) for x in range(2)])

        for line, point in zip(v_lines, cut_points[0]):
            line.move_to(point)

        self.play(ShowCreation(v_lines))
        self.wait()
        for target_points in cut_points[1:]:
            self.play(*[
                ApplyMethod(line.move_to, point)
                for line, point in zip(v_lines, target_points)
            ])
            self.wait()

        self.slice_indices = slice_indices
        self.v_lines = v_lines

    def show_continuous_fair_division(self):
        slice_indices = self.slice_indices

        groups = [
            VGroup(
                VGroup(*self.segments[i1:i2]),
                VGroup(*self.tick_marks[i1:i2]),
            )
            for i1, i2 in zip(slice_indices, slice_indices[1:])
        ]
        groups[-1].add(self.tick_marks[-1])
        vects = [[UP, DOWN][i] for i in self.forced_binary_choices]

        self.play(*[
            ApplyMethod(group.shift, 0.5*vect)
            for group, vect in zip(groups, vects)
        ])
        self.wait()

        self.groups = groups

    def set_color_continuous_groups(self):
        top_group = VGroup(self.groups[0], self.groups[2])
        bottom_group = self.groups[1]
        boxes = VGroup()
        for group in top_group, bottom_group:
            box = Rectangle(
                width = FRAME_WIDTH-2,
                height = group.get_height()+SMALL_BUFF,
                stroke_width = 0,
                fill_color = WHITE,
                fill_opacity = 0.25,
            )
            box.shift(group.get_center()[1]*UP)
            boxes.add(box)

        weight_description = VGroup(*[
            VGroup(
                TexMobject("\\frac{%d}{%d}"%(
                    len(jewel_type)/2, len(self.segments)
                )),
                Jewel(color = jewel_type[0].get_color())
            ).arrange()
            for jewel_type in self.jewel_types
        ])
        weight_description.arrange(buff = LARGE_BUFF)
        weight_description.next_to(boxes, UP, aligned_edge = LEFT)

        self.play(FadeIn(boxes))
        self.play(Write(weight_description))
        self.wait()

        self.set_color_box = boxes
        self.weight_description = weight_description

    def mention_equivalence_to_discrete_case(self):
        morty = Mortimer()
        morty.flip()
        morty.to_edge(DOWN)
        morty.shift(LEFT)
        self.play(FadeIn(morty))
        self.play(PiCreatureSays(
            morty, 
            """This is equivalent to
            the discrete case. """,
            bubble_kwargs = {
                "height" : 3, 
                "direction" : LEFT,
            }
        ))
        self.play(Blink(morty))
        self.wait()
        self.play(*list(map(FadeOut, [
            morty, morty.bubble, morty.bubble.content
        ])))
        
    def shift_divide_off_tick_marks(self):
        groups = self.groups
        slice_indices = self.slice_indices
        v_lines = self.v_lines

        left_segment = groups[1][0][0]
        left_tick = groups[1][1][0]
        right_segment = groups[-1][0][0]
        right_tick = groups[-1][1][0]

        segment_width = left_segment.get_width()

        for mob in left_segment, right_segment:
            mob.parts = VGroup(
                mob.copy().pointwise_become_partial(mob, 0, 0.5),
                mob.copy().pointwise_become_partial(mob, 0.5, 1),
            )
            self.remove(mob)
            self.add(mob.parts)
        restorers = [left_segment.parts, left_tick, right_segment.parts, right_tick]
        for mob in restorers:
            mob.save_state()

        emerald_segments = VGroup(*[
            segment
            for segment in list(groups[0][0])+list(groups[2][0])
            if segment.get_color() == Color(self.jewel_colors[1])
            if segment is not right_segment
        ])
        emerald_segments.add(
            left_segment.parts[0],
            right_segment.parts[1],
        )
        emerald_segments.sort()

        self.play(v_lines.shift, segment_width*RIGHT/2)
        self.play(*[
            ApplyMethod(mob.shift, vect)
            for mob, vect in [
                (left_segment.parts[0], UP),
                (left_tick, UP),
                (right_segment.parts[0], DOWN),
                (right_tick, DOWN),
            ]
        ])
        self.wait()

        words = TextMobject("Cut part way through segment")
        words.to_edge(RIGHT)
        words.shift(2*UP)
        arrow1 = Arrow(words.get_bottom(), left_segment.parts[0].get_right())
        arrow2 = Arrow(words.get_bottom(), right_segment.parts[1].get_left())
        VGroup(words, arrow1, arrow2).set_color(RED)

        self.play(Write(words), ShowCreation(arrow1))
        self.wait()

        emerald_segments.save_state()
        emerald_segments.generate_target()
        emerald_segments.target.arrange()
        emerald_segments.target.move_to(2*DOWN)
        brace = Brace(emerald_segments.target, DOWN)
        label = VGroup(
            TexMobject("5\\left( 1/18 \\right)"),
            Jewel(color = self.jewel_colors[1])
        ).arrange()
        label.next_to(brace, DOWN)
        self.play(MoveToTarget(emerald_segments))
        self.play(GrowFromCenter(brace))
        self.play(Write(label))
        self.wait()
        broken_pair = VGroup(*emerald_segments[2:4])
        broken_pair.save_state()
        self.play(broken_pair.shift, 0.5*UP)
        vect = broken_pair[1].get_left()-broken_pair[1].get_right()
        self.play(
            broken_pair[0].shift, -vect/2,
            broken_pair[1].shift, vect/2,
        )
        self.wait()
        self.play(broken_pair.space_out_submobjects)
        self.play(broken_pair.restore)
        self.wait()
        self.play(
            emerald_segments.restore,
            *list(map(FadeOut, [brace, label]))
        )

        self.wait()
        self.play(ShowCreation(arrow2))
        self.wait()
        self.play(*list(map(FadeOut, [words, arrow1, arrow2])))

        for line in v_lines:
            self.play(line.shift, segment_width*LEFT/2)
        self.play(*[mob.restore for mob in restorers])
        self.remove(left_segment.parts, right_segment.parts)
        self.add(left_segment, right_segment)

class ThinkAboutTheChoices(TeacherStudentsScene):
    def construct(self):
        self.teacher_says("""
            Think about the choices
            behind a division...
        """)
        self.change_student_modes(
            *["pondering"]*3,
            look_at_arg = FRAME_X_RADIUS*RIGHT+FRAME_Y_RADIUS*DOWN
        )
        self.wait(3)

class ChoicesInNecklaceCutting(ReconfigurableScene):
    CONFIG = {
        "num_continuous_division_searches" : 4,
        "denoms" : [6, 3, 2],
        "necklace_center" : DOWN,
        "thief_box_offset" : 1.2,
    }
    def construct(self):
        self.add_necklace()
        self.choose_places_to_cut()
        self.show_three_numbers_adding_to_one()
        self.make_binary_choice()

    def add_necklace(self):
        width, colors, num_per_color = [
            MakeTwoJewelCaseContinuous.CONFIG[key]
            for key in [
                "necklace_width", "jewel_colors", "num_per_jewel"
            ]
        ]
        color_list = list(it.chain(*[
            num*[color] 
            for num, color in zip(num_per_color, colors)
        ]))
        random.shuffle(color_list)

        interval = UnitInterval(
            tick_frequency = 1./sum(num_per_color),
            tick_size = 0.2,
            numbers_with_elongated_ticks = [],
        )
        interval.stretch_to_fit_width(width)
        interval.shift(self.necklace_center)
        tick_marks = interval.tick_marks
        tick_marks.set_stroke(WHITE, width = 2)

        segments = VGroup()
        for l_tick, r_tick, color in zip(tick_marks, tick_marks[1:], color_list):
            segment = Line(
                l_tick.get_center(),
                r_tick.get_center(),
                color = color
            )
            segments.add(segment)

        self.necklace = VGroup(segments, tick_marks)
        self.add(self.necklace)

        self.interval = interval        

    def choose_places_to_cut(self):
        v_lines = VGroup(*[DashedLine(UP, DOWN) for x in range(2)])
        final_num_pair = np.cumsum([1./d for d in self.denoms[:2]])

        num_pairs = [
            sorted([random.random(), random.random()])
            for x in range(self.num_continuous_division_searches)
        ] + [final_num_pair]

        point_pairs = [
            list(map(self.interval.number_to_point, num_pair))
            for num_pair in num_pairs
        ]
        
        for line, point in zip(v_lines, point_pairs[0]):
            line.move_to(point)
        self.play(ShowCreation(v_lines))
        for point_pair in point_pairs[1:]:
            self.wait()
            self.play(*[
                ApplyMethod(line.move_to, point)
                for line, point in zip(v_lines, point_pair)
            ])
        self.wait()

        self.division_points = list(it.chain(
            [self.interval.get_left()],
            point_pairs[-1],
            [self.interval.get_right()]
        ))

        self.v_lines = v_lines

    def show_three_numbers_adding_to_one(self):
        points = self.division_points
        braces = [
            Brace(Line(p1+SMALL_BUFF*RIGHT/2, p2+SMALL_BUFF*LEFT/2))
            for p1, p2 in zip(points, points[1:])
        ]
        for char, denom, brace in zip("abc", self.denoms, braces):
            brace.label = brace.get_text("$%s$"%char)
            brace.concrete_label = brace.get_text("$\\frac{1}{%d}$"%denom)
            VGroup(
                brace.label,
                brace.concrete_label
            ).set_color(YELLOW)

        words = TextMobject(
            "1) Choose", "$a$, $b$, $c$", "so that", "$a+b+c = 1$"
        )
        words[1].set_color(YELLOW)
        words[3].set_color(YELLOW)
        words.to_corner(UP+LEFT)

        self.play(*it.chain(*[
            [GrowFromCenter(brace), Write(brace.label)]
            for brace in braces
        ]))
        self.play(Write(words))
        self.wait(2)
        self.play(*[
            ReplacementTransform(brace.label, brace.concrete_label)
            for brace in braces
        ])
        self.wait()
        self.wiggle_v_lines()
        self.wait()
        self.transition_to_alt_config(denoms = [3, 3, 3])
        self.wait()
        self.play(*list(map(FadeOut, list(braces) + [
            brace.concrete_label for brace in braces
        ])))

        self.choice_one_words = words

    def make_binary_choice(self):
        groups = self.get_groups()
        boxes, labels = self.get_boxes_and_labels()
        arrow_pairs, curr_arrows = self.get_choice_arrow_pairs(groups)
        words = TextMobject("2) Make a binary choice for each segment")
        words.next_to(
            self.choice_one_words, DOWN, 
            buff = MED_LARGE_BUFF, 
            aligned_edge = LEFT
        )

        self.play(Write(words))
        self.play(*list(map(FadeIn, [boxes, labels])))
        for binary_choices in it.product(*[[0, 1]]*3):
            self.play(*[
                ApplyMethod(group.move_to, group.target_points[choice])
                for group, choice in zip(groups, binary_choices)
            ] + [
                Transform(
                    curr_arrow, arrow_pair[choice],
                    path_arc = np.pi
                )
                for curr_arrow, arrow_pair, choice in zip(
                    curr_arrows, arrow_pairs, binary_choices
                )
            ])
            self.wait()

    ######

    def get_groups(self, indices = None):
        segments, tick_marks = self.necklace
        if indices is None:
            n_segments = len(segments)
            indices = [0, n_segments/6, n_segments/2, n_segments]

        groups = [
            VGroup(
                VGroup(*segments[i1:i2]),
                VGroup(*tick_marks[i1:i2]),
            )
            for i1, i2 in zip(indices, indices[1:])
        ]
        for group, index in zip(groups, indices[1:]):
            group[1].add(tick_marks[index].copy())
        groups[-1][1].add(tick_marks[-1])

        for group in groups:
            group.target_points = [
                group.get_center() + self.thief_box_offset*vect
                for vect in (UP, DOWN)
            ]

        return groups

    def get_boxes_and_labels(self):
        box = Rectangle(
            height = self.necklace.get_height()+SMALL_BUFF,
            width = self.necklace.get_width()+2*SMALL_BUFF,
            stroke_width = 0,
            fill_color = WHITE,
            fill_opacity = 0.25
        )
        box.move_to(self.necklace)

        boxes = VGroup(*[
            box.copy().shift(self.thief_box_offset*vect)
            for vect in (UP, DOWN)
        ])
        labels = VGroup(*[
            TextMobject(
                "Thief %d"%(i+1)
            ).next_to(box, UP, aligned_edge = RIGHT)
            for i, box in enumerate(boxes)
        ])
        return boxes, labels

    def get_choice_arrow_pairs(self, groups):
        arrow = TexMobject("\\uparrow")
        arrow_pairs = [
            [arrow.copy(), arrow.copy().rotate(np.pi)]
            for group in groups
        ]
        pre_arrow_points = [
            VectorizedPoint(group.get_center())
            for group in groups
        ]
        for point, arrow_pair in zip(pre_arrow_points, arrow_pairs):
            for arrow, color in zip(arrow_pair, [GREEN, RED]):
                arrow.set_color(color)
                arrow.move_to(point.get_center())
        return arrow_pairs, pre_arrow_points

    def wiggle_v_lines(self):
        self.play(
            *it.chain(*[
                [
                    line.rotate_in_place, np.pi/12, vect,
                    line.set_color, RED
                ]
                for line, vect in zip(self.v_lines, [OUT, IN])
            ]),
            rate_func = wiggle
        )

class CompareThisToSphereChoice(TeacherStudentsScene):
    def construct(self):
        self.teacher_says("""
            Compare this to choosing
            a point on the sphere.
        """)
        self.change_student_modes(
            *["pondering"]*3,
            look_at_arg = FRAME_X_RADIUS*RIGHT+FRAME_Y_RADIUS*DOWN
        )
        self.wait(3)

class SimpleRotatingSphereWithPoint(ExternallyAnimatedScene):
    pass

class ChoicesForSpherePoint(GeneralizeBorsukUlam):
    def construct(self):
        self.add_sphere_set()
        self.initialize_words()
        self.play(Write(self.choice_one_words))
        self.wait()
        self.show_example_choices()
        self.show_binary_choices()

    def get_tuple(self):
        tup = TexMobject("(x, y, z)")
        for i, color in zip([1, 3, 5], self.colors):
            tup[i].set_color(color)
        return tup

    def get_condition(self):
        condition = TexMobject("x^2+y^2+z^2 = 1")
        for i, color in zip([0, 3, 6], self.colors):
            VGroup(*condition[i:i+2]).set_color(color)
        return condition

    def add_sphere_set(self):
        sphere_set = self.get_sphere_set()
        sphere_set.scale(0.7)
        sphere_set.to_edge(RIGHT)
        sphere_set.shift(UP)

        self.add(sphere_set)
        self.sphere_set = sphere_set

    def initialize_words(self):
        choice_one_words = TextMobject(
            "1) Choose", "$x^2$, $y^2$, $z^2$",
            "so that", "$x^2+y^2+z^2 = 1$"
        )
        for i in 1, 3:
            for j, color in zip([0, 3, 6], self.colors):
                VGroup(*choice_one_words[i][j:j+2]).set_color(color)
        choice_one_words.to_corner(UP+LEFT)

        choice_two_words = TextMobject(
            "2) Make a binary choice for each one"
        )
        choice_two_words.next_to(
            choice_one_words, DOWN, 
            buff = MED_LARGE_BUFF,
            aligned_edge = LEFT
        )

        self.choice_one_words = choice_one_words
        self.choice_two_words = choice_two_words

    def show_example_choices(self):
        choices = VGroup(*[
            TexMobject(*tex).set_color(color)
            for color, tex in zip(self.colors, [
                ("x", "^2 = ", "1/6"),
                ("y", "^2 = ", "1/3"),
                ("z", "^2 = ", "1/2"),
            ])
        ])
        choices.arrange(
            DOWN, 
            buff = LARGE_BUFF,
            aligned_edge = LEFT
        )
        choices.set_height(FRAME_Y_RADIUS)
        choices.to_edge(LEFT)
        choices.shift(DOWN)

        self.play(FadeIn(
            choices,
            run_time = 2,
            lag_ratio = 0.5
        ))
        self.wait()

        self.choices = choices

    def show_binary_choices(self):
        for choice in self.choices:
            var_tex = choice.expression_parts[0]
            frac_tex = choice.expression_parts[2]
            sqrts = VGroup(*[
                TexMobject(
                    var_tex + "=" + sign + \
                    "\\sqrt{%s}"%frac_tex)
                for sign in ["+", "-"]
            ])
            for sqrt in sqrts:
                sqrt.scale(0.6)
            sqrts.arrange(DOWN)
            sqrts.next_to(choice, RIGHT, buff = LARGE_BUFF)
            sqrts.set_color(choice.get_color())

            arrows = VGroup(*[
                Arrow(
                    choice.get_right(), sqrt.get_left(), 
                    color = WHITE,
                    tip_length = 0.1,
                    buff = SMALL_BUFF
                )
                for sqrt in sqrts
            ])

            self.play(ShowCreation(arrows))
            self.play(FadeIn(sqrts, lag_ratio = 0.5))
        self.play(Write(self.choice_two_words))
        self.wait()

class NecklaceDivisionSphereAssociation(ChoicesInNecklaceCutting):
    CONFIG = {
        "xyz_colors" : color_gradient([GREEN_B, BLUE], 3),
        "necklace_center" : DOWN,
        "thief_box_offset" : 1.6,
        "denoms" : [6, 3, 2],
    }
    def construct(self):
        self.add_necklace()
        self.add_sphere_point_label()
        self.choose_places_to_cut()
        self.add_braces()
        self.add_boxes_and_labels()
        self.show_binary_choice_association()
        self.ask_about_antipodal_pairs()

    def add_sphere_point_label(self):
        label = TextMobject(
            "$(x, y, z)$",
            "such that",
            "$x^2 + y^2 + z^2 = 1$"
        )
        for i, j_list in (0, [1, 3, 5]), (2, [0, 3, 6]):
            for j, color in zip(j_list, self.xyz_colors):
                label[i][j].set_color(color)
        label.to_corner(UP+RIGHT)

        ghost_sphere_point = VectorizedPoint()
        ghost_sphere_point.to_corner(UP+LEFT, buff = LARGE_BUFF)
        ghost_sphere_point.shift(2*RIGHT)

        arrow = Arrow(
            label.get_left(), ghost_sphere_point,
            color = WHITE
        )

        self.add(label, arrow)

        self.sphere_point_label = label

    def add_braces(self):
        points = self.division_points
        braces = [
            Brace(
                Line(p1+SMALL_BUFF*RIGHT/2, p2+SMALL_BUFF*LEFT/2),
                UP
            )
            for p1, p2 in zip(points, points[1:])
        ]
        for char, brace, color, denom in zip("xyz", braces, self.xyz_colors, self.denoms):
            brace.label = brace.get_text(
                "$%s^2$"%char, "$= 1/%d$"%denom,
                buff = SMALL_BUFF
            )
            brace.label.set_color(color)
            if brace.label.get_right()[0] > brace.get_right()[0]:
                brace.label.next_to(
                    brace, UP, buff = SMALL_BUFF,
                    aligned_edge = RIGHT
                )

        self.play(*it.chain(
            list(map(GrowFromCenter, braces)),
            [Write(brace.label) for brace in braces]
        ))
        self.wait()

        self.braces = braces

    def add_boxes_and_labels(self):
        boxes, labels = self.get_boxes_and_labels()
        self.play(*list(map(FadeIn, [boxes, labels])))
        self.wait()

    def show_binary_choice_association(self):
        groups = self.get_groups()
        self.swapping_anims = []
        final_choices = [1, 0, 1]
        quads = list(zip(self.braces, self.denoms, groups, final_choices))
        for brace, denom, group, final_choice in quads:
            char = brace.label.args[0][1]
            choices = [
                TexMobject(
                    char, "=", sign, "\\sqrt{\\frac{1}{%d}}"%denom
                )
                for sign in ("+", "-")
            ]
            for choice, color in zip(choices, [GREEN, RED]):
                # choice[0].set_color(brace.label.get_color())
                choice[2].set_color(color)
                choice.scale(0.8)
                choice.move_to(group)
                if choice.get_width() > 0.8*group.get_width():
                    choice.next_to(group.get_right(), LEFT, buff = MED_SMALL_BUFF)
            original_choices = [m.copy() for m in choices]

            self.play(
                ReplacementTransform(
                    VGroup(brace.label[0], brace, brace.label[1]), 
                    choices[0]
                ),
                group.move_to, group.target_points[0]
            )
            self.wait()
            self.play(
                Transform(*choices),
                group.move_to, group.target_points[1]
            )
            self.wait()
            if final_choice == 0:
                self.play(
                    Transform(choices[0], original_choices[0]),
                    group.move_to, group.target_points[0]
                )
            self.swapping_anims += [
                Transform(choices[0], original_choices[1-final_choice]),
                group.move_to, group.target_points[1-final_choice]
            ]

    def ask_about_antipodal_pairs(self):
        question = TextMobject("What do antipodal points signify?")
        question.move_to(self.sphere_point_label, LEFT)
        question.set_color(MAROON_B)
        antipodal_tex = TexMobject(
            "(x, y, z) \\rightarrow (-x, -y, -z)"
        )
        antipodal_tex.next_to(question, DOWN, aligned_edge = LEFT)

        self.play(FadeOut(self.sphere_point_label))
        self.play(FadeIn(question))
        self.wait()
        self.play(Write(antipodal_tex))
        self.wait()
        self.wiggle_v_lines()
        self.wait()
        self.play(*self.swapping_anims)
        self.wait()

class SimpleRotatingSphereWithAntipodes(ExternallyAnimatedScene):
    pass

class TotalLengthOfEachJewelEquals(NecklaceDivisionSphereAssociation, ThreeDScene):
    CONFIG = {
        "random_seed" : 1,
        "thief_box_offset" : 1.2,
    }
    def construct(self):
        random.seed(self.random_seed)
        self.add_necklace()
        self.add_boxes_and_labels()
        self.find_fair_division()
        self.demonstrate_fair_division()
        self.perform_antipodal_swap()

    def find_fair_division(self):
        segments, tick_marks = self.necklace
        segments.sort()
        segment_colors = [
            segment.get_color()
            for segment in segments
        ]
        indices = self.get_fair_division_indices(segment_colors)
        groups = self.get_groups(
            [0] + list(np.array(indices)+1) + [len(segments)]
        )
        self.add(*groups)
        binary_choice = [0, 1, 0]

        v_lines = VGroup(*[DashedLine(UP, DOWN) for x in range(2)])
        v_lines.move_to(self.necklace)
        self.play(ShowCreation(v_lines))
        self.play(*[
            ApplyMethod(line.move_to, segments[index].get_right())
            for line, index in zip(v_lines, indices)
        ])
        self.wait()
        self.play(*[
            ApplyMethod(group.move_to, group.target_points[choice])
            for group, choice in zip(groups, binary_choice)
        ])
        self.wait()

        self.groups = groups
        self.v_lines = v_lines

    def get_fair_division_indices(self, colors):
        colors = np.array(list(colors))
        color_types = list(map(Color, set([c.get_hex_l() for c in colors])))
        type_to_count = dict([
            (color, sum(colors == color))
            for color in color_types
        ])
        for i1, i2 in it.combinations(list(range(1, len(colors)-1)), 2):
            bools = [
                sum(colors[i1:i2] == color) == type_to_count[color]/2
                for color in color_types
            ]
            if np.all(bools):
                return i1, i2
        raise Exception("No fair division found")

    def demonstrate_fair_division(self):
        segments, tick_marks = self.necklace
        color_types = list(map(Color, set([
            segment.get_color().get_hex_l()
            for segment in segments
        ])))
        top_segments = VGroup(*it.chain(
            self.groups[0][0],
            self.groups[2][0],
        ))
        bottom_segments = self.groups[1][0]
        for color in color_types:
            monochrome_groups = [
                VGroup(*[segment for segment in segment_group if segment.get_color() == color])
                for segment_group in (top_segments, bottom_segments)
            ]
            labels = VGroup()
            for i, group in enumerate(monochrome_groups):
                group.save_state()
                group.generate_target()
                group.target.arrange(buff = SMALL_BUFF)
                brace = Brace(group.target, UP)
                label = VGroup(
                    TextMobject("Thief %d"%(i+1)),
                    Jewel(color = group[0].get_color())
                )
                label.arrange()
                label.next_to(brace, UP)
                full_group = VGroup(group.target, brace, label)
                vect = LEFT if i == 0 else RIGHT
                full_group.next_to(ORIGIN, vect, buff = MED_LARGE_BUFF)
                full_group.to_edge(UP)
                labels.add(VGroup(brace, label))
            equals = TexMobject("=")
            equals.next_to(monochrome_groups[0].target, RIGHT)
            labels[-1].add(equals)

            for group, label in zip(monochrome_groups, labels):
                self.play(
                    MoveToTarget(group),
                    FadeIn(label),
                )
                self.wait()
            self.play(
                FadeOut(labels),
                *[group.restore for group in monochrome_groups]
            )
        self.wait()

    def perform_antipodal_swap(self):
        binary_choices_list = [(1, 0, 1), (0, 1, 0)]
        for binary_choices in binary_choices_list:
            self.play(*[
                ApplyMethod(
                    group.move_to,
                    group.target_points[choice]
                )
                for group, choice in zip(self.groups, binary_choices)
            ])
            self.wait()

class ExclaimBorsukUlam(TeacherStudentsScene):
    def construct(self):
        self.student_says(
            "Borsuk-Ulam!",
            target_mode = "hooray"
        )
        self.play(*[
            ApplyMethod(pi.change_mode, "hooray")
            for pi in self.get_pi_creatures()
        ])
        self.wait(3)

class ShowFunctionDiagram(TotalLengthOfEachJewelEquals, ReconfigurableScene):
    CONFIG = {
        "necklace_center" : ORIGIN,
        "camera_class" : ThreeDCamera,
        "thief_box_offset" : 0.3,
        "make_up_fair_division_indices" : False,
    }
    def construct(self):
        self.add_necklace()
        self.add_number_pair()
        self.swap_necklace_allocation()
        self.add_sphere_arrow()

    def add_necklace(self):
        random.seed(self.random_seed)
        ChoicesInNecklaceCutting.add_necklace(self)
        self.necklace.set_width(FRAME_X_RADIUS-1)
        self.necklace.to_edge(UP, buff = LARGE_BUFF)
        self.necklace.to_edge(LEFT, buff = SMALL_BUFF)
        self.add(self.necklace)

        self.find_fair_division()

    def add_number_pair(self):
        plane_classes = [
            JewelPairPlane(
                skip_animations = True, 
                thief_number = x
            )
            for x in (1, 2)
        ]
        t1_plane, t2_plane = planes = VGroup(*[
            VGroup(*plane_class.get_top_level_mobjects())
            for plane_class in plane_classes
        ])
        planes.set_width(FRAME_X_RADIUS)
        planes.to_edge(RIGHT)
        self.example_coords = plane_classes[0].example_coords[0]

        arrow = Arrow(
            self.necklace.get_corner(DOWN+RIGHT), 
            self.example_coords,
            color = YELLOW
        )

        self.play(ShowCreation(arrow))
        self.play(Write(t1_plane), Animation(arrow))
        self.wait()
        clean_state = VGroup(*self.mobjects).family_members_with_points()
        self.clear()
        self.add(*clean_state)
        self.transition_to_alt_config(
            make_up_fair_division_indices = True
        )
        self.wait()
        t1_plane.save_state()
        self.play(
            Transform(*planes, path_arc = np.pi),
            Animation(arrow)
        )
        self.wait(2)
        self.play(
            ApplyMethod(t1_plane.restore, path_arc = np.pi),
            Animation(arrow)
        )
        self.wait()

    def swap_necklace_allocation(self):
        for choices in [(1, 0, 1), (0, 1, 0)]:
            self.play(*[
                ApplyMethod(group.move_to, group.target_points[i])
                for group, i in zip(self.groups, choices)
            ])
            self.wait()

    def add_sphere_arrow(self):
        up_down_arrow = TexMobject("\\updownarrow")
        up_down_arrow.scale(1.5)
        up_down_arrow.set_color(YELLOW)
        up_down_arrow.next_to(self.necklace, DOWN, buff = LARGE_BUFF)

        to_plane_arrow = Arrow(
            up_down_arrow.get_bottom() + DOWN+RIGHT,
            self.example_coords,
            color = YELLOW
        )

        self.play(Write(up_down_arrow))
        self.wait()
        self.play(ShowCreation(to_plane_arrow))
        self.wait()

    def get_fair_division_indices(self, *args):
        if self.make_up_fair_division_indices:
            return [9, 14]
        else:
            return TotalLengthOfEachJewelEquals.get_fair_division_indices(self, *args)

class JewelPairPlane(GraphScene):
    CONFIG = {
        "camera_class" : ThreeDCamera,
        "x_labeled_nums" : [],
        "y_labeled_nums" : [],
        "thief_number" : 1,
        "colors" : [BLUE, GREEN],
    }
    def construct(self):
        self.setup_axes()
        point = self.coords_to_point(4, 5)
        dot = Dot(point, color = WHITE)
        coord_pair = TexMobject(
            "\\big(", 
            "\\text{Thief %d }"%self.thief_number, "X", ",", 
            "\\text{Thief %d }"%self.thief_number, "X", 
            "\\big)"
        )
        # coord_pair.scale(1.5)
        to_replace = [coord_pair[i] for i in [2, 5]]
        for mob, color in zip(to_replace, self.colors):
            jewel = Jewel(color = color)
            jewel.replace(mob)
            coord_pair.remove(mob)
            coord_pair.add(jewel)
        coord_pair.next_to(dot, UP+RIGHT, buff = 0)

        self.example_coords = VGroup(dot, coord_pair)
        self.add(self.example_coords)

class WhatThisMappingActuallyLooksLikeWords(Scene):
    def construct(self):
        words = TextMobject("What this mapping actually looks like")
        words.set_width(FRAME_WIDTH-1)
        words.to_edge(DOWN)

        self.play(Write(words))
        self.wait()

class WhatAboutGeneralCase(TeacherStudentsScene):
    def construct(self):
        self.student_says("""
            What about when
            there's more than 2 jewels?
        """)
        self.change_student_modes("confused", None, "sassy")
        self.wait()
        self.play(self.get_teacher().change_mode, "thinking")
        self.wait()
        self.teacher_says(
            """Use Borsuk-Ulam for
            higher-dimensional spheres """, 
            target_mode = "hooray"
        )
        self.change_student_modes(*["confused"]*3)
        self.wait(2)

class Simple3DSpace(ExternallyAnimatedScene):
    pass

class FourDBorsukUlam(GeneralizeBorsukUlam, PiCreatureScene):
    CONFIG = {
        "n_dims" : 4,
        "use_morty" : False,
    }
    def setup(self):
        GeneralizeBorsukUlam.setup(self)
        PiCreatureScene.setup(self)
        self.pi_creature.to_corner(DOWN+LEFT, buff = MED_SMALL_BUFF)

    def construct(self):
        sphere_set = self.get_sphere_set()
        arrow = Arrow(LEFT, RIGHT)
        f = TexMobject("f")
        output_space = self.get_output_space()
        equation = self.get_equation()

        sphere_set.to_corner(UP+LEFT)
        arrow.next_to(sphere_set, RIGHT)
        f.next_to(arrow, UP)
        output_space.next_to(arrow, RIGHT)
        equation.next_to(sphere_set, DOWN, buff = LARGE_BUFF)
        equation.to_edge(RIGHT)
        lhs = VGroup(*equation[:2])
        eq = equation[2]
        rhs = VGroup(*equation[3:])

        brace = Brace(Line(ORIGIN, 5*RIGHT))
        brace.to_edge(RIGHT)
        brace_text = brace.get_text("Triplets of numbers")
        brace_text.shift_onto_screen()

        self.play(FadeIn(sphere_set))
        self.change_mode("confused")
        self.wait()
        self.play(
            ShowCreation(arrow),
            Write(f)
        )
        self.play(Write(output_space))
        self.wait()
        self.change_mode("maybe")
        self.wait(2)
        self.change_mode("pondering")
        self.wait()
        self.play(
            GrowFromCenter(brace),
            Write(brace_text)
        )
        self.wait()
        self.play(*list(map(FadeOut, [brace, brace_text])))
        self.wait()
        self.play(
            FadeIn(lhs),
            self.pi_creature.change_mode, "raise_right_hand"
        )
        self.play(
            ReplacementTransform(lhs.copy(), rhs),
            Write(eq)
        )
        self.wait(2)

    def get_sphere_set(self):
        sphere_set = GeneralizeBorsukUlam.get_sphere_set(self)
        brace = Brace(sphere_set)
        text = brace.get_text("Hypersphere in 4D")
        sphere_set.add(brace, text)
        return sphere_set

class CircleToSphereToQMarks(Scene):
    def construct(self):
        pi_groups = VGroup()
        modes = ["happy", "pondering", "pleading"]
        shapes = [
            Circle(color = BLUE, radius = 0.5), 
            VectorizedPoint(), 
            TexMobject("???")
        ]
        for d, mode, shape in zip(it.count(2), modes, shapes):
            randy = Randolph(mode = mode)
            randy.scale(0.7)
            bubble = randy.get_bubble(
                height = 3, width = 4,
                direction = LEFT
            )
            bubble.pin_to(randy)
            bubble.position_mobject_inside(shape)
            title = TextMobject("%dD"%d)
            title.next_to(randy, UP)
            arrow = Arrow(LEFT, RIGHT)
            arrow.next_to(randy.get_corner(UP+RIGHT))
            pi_groups.add(VGroup(
                randy, bubble, shape, title, arrow
            ))

        pi_groups[-1].remove(pi_groups[-1][-1])
        pi_groups.arrange(buff = -1)
        for mob in pi_groups:
            self.play(FadeIn(mob))
        self.wait(2)
        self.play(pi_groups[-1][0].change_mode, "thinking")
        self.wait(2)

class BorsukPatreonThanks(PatreonThanks):
    CONFIG = {
        "specific_patrons" : [
            "Ali  Yahya",
            "Meshal  Alshammari",
            "CrypticSwarm    ",
            "Ankit   Agarwal",
            "Yu  Jun",
            "Shelby  Doolittle",
            "Dave    Nicponski",
            "Damion  Kistler",
            "Juan    Benet",
            "Othman  Alikhan",
            "Justin Helps",
            "Markus  Persson",
            "Dan Buchoff",
            "Derek   Dai",
            "Joseph  John Cox",
            "Luc Ritchie",
            "Guido   Gambardella",
            "Jerry   Ling",
            "Mark    Govea",
            "Vecht",
            "Jonathan    Eppele",
            "Shimin Kuang",
            "Rish    Kundalia",
            "Achille Brighton",
            "Kirk    Werklund",
            "Ripta   Pasay",
            "Felipe  Diniz",
        ]
    }

class MortyLookingAtRectangle(Scene):
    def construct(self):
        morty = Mortimer()
        morty.to_corner(DOWN+RIGHT)
        url = TextMobject("www.thegreatcoursesplus.com/3blue1brown")
        url.scale(0.75)
        url.to_corner(UP+LEFT)
        rect = Rectangle(height = 9, width = 16)
        rect.set_height(5)
        rect.next_to(url, DOWN)
        rect.shift_onto_screen()
        url.save_state()
        url.next_to(morty.get_corner(UP+LEFT), UP)
        url.shift_onto_screen()

        self.add(morty)
        self.play(
            morty.change_mode, "raise_right_hand",
            morty.look_at, url,
        )
        self.play(Write(url))
        self.play(Blink(morty))
        self.wait()
        self.play(
            url.restore,
            morty.change_mode, "happy"
        )
        self.play(ShowCreation(rect))
        self.wait()
        self.play(Blink(morty))
        for mode in ["pondering", "hooray", "happy", "pondering", "happy"]:
            self.play(morty.change_mode, mode)
            self.wait(2)
            self.play(Blink(morty))
            self.wait(2)

class RotatingThreeDSphereProjection(Scene):
    CONFIG = {
        "camera_class" : ThreeDCamera,
    }
    def construct(self):
        sphere = VGroup(*[
            Circle(radius = np.sin(t)).shift(np.cos(t)*OUT)
            for t in np.linspace(0, np.pi, 20)
        ])
        sphere.set_stroke(BLUE, width = 2)
        # sphere.set_fill(BLUE, opacity = 0.1)

        self.play(Rotating(
            sphere, axis = RIGHT+OUT,
            run_time = 10
        ))
        self.repeat_frames(4)

class FourDSphereProjectTo4D(ExternallyAnimatedScene):
    pass


class Test(Scene):
    CONFIG = {
        "camera_class" : ThreeDCamera,
    }
    def construct(self):
        randy = Randolph()
        necklace = Necklace()
        necklace.insert_n_curves(20)
        # necklace.apply_function(
        #     lambda (x, y, z) : x*RIGHT + (y + 0.1*x**2)*UP
        # )
        necklace.set_width(randy.get_width() + 1)
        necklace.move_to(randy)

        self.add(randy, necklace)













