from pathlib import Path

import manim.utils.opengl as opengl
from manim import *
from manim.opengl import *

# Copied from https://3b1b.github.io/manim/getting_started/example_scenes.html#surfaceexample.
# Lines that do not yet work with the Community Version are commented.


def get_plane_mesh(context):
    shader = Shader(context, name="vertex_colors")
    attributes = np.zeros(
        18,
        dtype=[
            ("in_vert", np.float32, (4,)),
            ("in_color", np.float32, (4,)),
        ],
    )
    attributes["in_vert"] = np.array(
        [
            # xy plane
            [-1, -1, 0, 1],
            [-1, 1, 0, 1],
            [1, 1, 0, 1],
            [-1, -1, 0, 1],
            [1, -1, 0, 1],
            [1, 1, 0, 1],
            # yz plane
            [0, -1, -1, 1],
            [0, -1, 1, 1],
            [0, 1, 1, 1],
            [0, -1, -1, 1],
            [0, 1, -1, 1],
            [0, 1, 1, 1],
            # xz plane
            [-1, 0, -1, 1],
            [-1, 0, 1, 1],
            [1, 0, 1, 1],
            [-1, 0, -1, 1],
            [1, 0, -1, 1],
            [1, 0, 1, 1],
        ],
    )
    attributes["in_color"] = np.array(
        [
            # xy plane
            [1, 0, 0, 1],
            [1, 0, 0, 1],
            [1, 0, 0, 1],
            [1, 0, 0, 1],
            [1, 0, 0, 1],
            [1, 0, 0, 1],
            # yz plane
            [0, 1, 0, 1],
            [0, 1, 0, 1],
            [0, 1, 0, 1],
            [0, 1, 0, 1],
            [0, 1, 0, 1],
            [0, 1, 0, 1],
            # xz plane
            [0, 0, 1, 1],
            [0, 0, 1, 1],
            [0, 0, 1, 1],
            [0, 0, 1, 1],
            [0, 0, 1, 1],
            [0, 0, 1, 1],
        ],
    )
    return Mesh(shader, attributes)


class TextTest(Scene):
    def construct(self):
        import string

        text = Text(string.ascii_lowercase, stroke_width=4, stroke_color=BLUE).scale(2)
        text2 = (
            Text(string.ascii_uppercase, stroke_width=4, stroke_color=BLUE)
            .scale(2)
            .next_to(text, DOWN)
        )
        # self.add(text, text2)
        self.play(Write(text))
        self.play(Write(text2))
        self.interactive_embed()


class GuiTest(Scene):
    def construct(self):
        mesh = get_plane_mesh(self.renderer.context)
        # mesh.attributes["in_vert"][:, 0]
        self.add(mesh)

        def update_mesh(mesh, dt):
            mesh.model_matrix = np.matmul(
                opengl.rotation_matrix(z=dt),
                mesh.model_matrix,
            )

        mesh.add_updater(update_mesh)

        self.interactive_embed()


class GuiTest2(Scene):
    def construct(self):
        mesh = get_plane_mesh(self.renderer.context)
        mesh.attributes["in_vert"][:, 0] -= 2
        self.add(mesh)

        mesh2 = get_plane_mesh(self.renderer.context)
        mesh2.attributes["in_vert"][:, 0] += 2
        self.add(mesh2)

        def callback(sender, data):
            mesh2.attributes["in_color"][:, 3] = dpg.get_value(sender)

        self.widgets.append(
            {
                "name": "mesh2 opacity",
                "widget": "slider_float",
                "callback": callback,
                "min_value": 0,
                "max_value": 1,
                "default_value": 1,
            },
        )

        self.interactive_embed()


class ThreeDMobjectTest(Scene):
    def construct(self):
        # config["background_color"] = "#333333"

        s = Square(fill_opacity=0.5).shift(2 * RIGHT)
        self.add(s)

        sp = Sphere().shift(2 * LEFT)
        self.add(sp)

        mesh = get_plane_mesh(self.renderer.context)
        self.add(mesh)

        def update_mesh(mesh, dt):
            mesh.model_matrix = np.matmul(
                opengl.rotation_matrix(z=dt),
                mesh.model_matrix,
            )

        mesh.add_updater(update_mesh)

        self.interactive_embed()


class NamedFullScreenQuad(Scene):
    def construct(self):
        surface = FullScreenQuad(self.renderer.context, fragment_shader_name="design_3")
        surface.shader.set_uniform(
            "u_resolution",
            (config["pixel_width"], config["pixel_height"], 0.0),
        )
        surface.shader.set_uniform("u_time", 0)
        self.add(surface)

        t = 0

        def update_surface(surface, dt):
            nonlocal t
            t += dt
            surface.shader.set_uniform("u_time", t / 4)

        surface.add_updater(update_surface)

        # self.wait()
        self.interactive_embed()


class InlineFullScreenQuad(Scene):
    def construct(self):
        surface = FullScreenQuad(
            self.renderer.context,
            """
            #version 330


            #define TWO_PI 6.28318530718

            uniform vec2 u_resolution;
            uniform float u_time;
            out vec4 frag_color;

            //  Function from Iñigo Quiles
            //  https://www.shadertoy.com/view/MsS3Wc
            vec3 hsb2rgb( in vec3 c ){
                vec3 rgb = clamp(abs(mod(c.x*6.0+vec3(0.0,4.0,2.0),
                                         6.0)-3.0)-1.0,
                                 0.0,
                                 1.0 );
                rgb = rgb*rgb*(3.0-2.0*rgb);
                return c.z * mix( vec3(1.0), rgb, c.y);
            }

            void main(){
                vec2 st = gl_FragCoord.xy/u_resolution;
                vec3 color = vec3(0.0);

                // Use polar coordinates instead of cartesian
                vec2 toCenter = vec2(0.5)-st;
                float angle = atan(toCenter.y,toCenter.x);
                angle += u_time;
                float radius = length(toCenter)*2.0;

                // Map the angle (-PI to PI) to the Hue (from 0 to 1)
                // and the Saturation to the radius
                color = hsb2rgb(vec3((angle/TWO_PI)+0.5,radius,1.0));

                frag_color = vec4(color,1.0);
            }
            """,
        )
        surface.shader.set_uniform(
            "u_resolution",
            (config["pixel_width"], config["pixel_height"]),
        )
        shader_time = 0

        def update_surface(surface):
            nonlocal shader_time
            surface.shader.set_uniform("u_time", shader_time)
            shader_time += 1 / 60.0

        surface.add_updater(update_surface)
        self.add(surface)
        # self.wait(5)
        self.interactive_embed()


class SimpleInlineFullScreenQuad(Scene):
    def construct(self):
        surface = FullScreenQuad(
            self.renderer.context,
            """
            #version 330

            uniform float v_red;
            uniform float v_green;
            uniform float v_blue;
            out vec4 frag_color;

            void main() {
              frag_color = vec4(v_red, v_green, v_blue, 1);
            }
            """,
        )
        surface.shader.set_uniform("v_red", 0)
        surface.shader.set_uniform("v_green", 0)
        surface.shader.set_uniform("v_blue", 0)

        increase = True
        val = 0.5
        surface.shader.set_uniform("v_red", val)
        surface.shader.set_uniform("v_green", val)
        surface.shader.set_uniform("v_blue", val)

        def update_surface(mesh, dt):
            nonlocal increase
            nonlocal val
            if increase:
                val += dt
            else:
                val -= dt
            if val >= 1:
                increase = False
            elif val <= 0:
                increase = True
            surface.shader.set_uniform("v_red", val)
            surface.shader.set_uniform("v_green", val)
            surface.shader.set_uniform("v_blue", val)

        surface.add_updater(update_surface)

        self.add(surface)
        self.wait(5)


class InlineShaderExample(Scene):
    def construct(self):
        config["background_color"] = "#333333"

        c = Circle(fill_opacity=0.7).shift(UL)
        self.add(c)

        shader = Shader(
            self.renderer.context,
            source={
                "vertex_shader": """
                #version 330

                in vec4 in_vert;
                in vec4 in_color;
                out vec4 v_color;
                uniform mat4 u_model_view_matrix;
                uniform mat4 u_projection_matrix;

                void main() {
                    v_color = in_color;
                    vec4 camera_space_vertex = u_model_view_matrix * in_vert;
                    vec4 clip_space_vertex = u_projection_matrix * camera_space_vertex;
                    gl_Position = clip_space_vertex;
                }
            """,
                "fragment_shader": """
            #version 330

            in vec4 v_color;
            out vec4 frag_color;

            void main() {
              frag_color = v_color;
            }
            """,
            },
        )
        shader.set_uniform("u_model_view_matrix", opengl.view_matrix())
        shader.set_uniform(
            "u_projection_matrix",
            opengl.orthographic_projection_matrix(),
        )

        attributes = np.zeros(
            6,
            dtype=[
                ("in_vert", np.float32, (4,)),
                ("in_color", np.float32, (4,)),
            ],
        )
        attributes["in_vert"] = np.array(
            [
                [-1, -1, 0, 1],
                [-1, 1, 0, 1],
                [1, 1, 0, 1],
                [-1, -1, 0, 1],
                [1, -1, 0, 1],
                [1, 1, 0, 1],
            ],
        )
        attributes["in_color"] = np.array(
            [
                [0, 0, 1, 1],
                [0, 0, 1, 1],
                [0, 0, 1, 1],
                [0, 0, 1, 1],
                [0, 0, 1, 1],
                [0, 0, 1, 1],
            ],
        )
        mesh = Mesh(shader, attributes)
        self.add(mesh)

        self.wait(5)
        # self.embed_2()


class NamedShaderExample(Scene):
    def construct(self):
        shader = Shader(self.renderer.context, "manim_coords")
        shader.set_uniform("u_color", (0.0, 1.0, 0.0, 1.0))

        view_matrix = self.camera.formatted_view_matrix
        shader.set_uniform("u_model_view_matrix", view_matrix)
        shader.set_uniform(
            "u_projection_matrix",
            opengl.perspective_projection_matrix(),
        )
        attributes = np.zeros(
            6,
            dtype=[
                ("in_vert", np.float32, (4,)),
            ],
        )
        attributes["in_vert"] = np.array(
            [
                [-1, -1, 0, 1],
                [-1, 1, 0, 1],
                [1, 1, 0, 1],
                [-1, -1, 0, 1],
                [1, -1, 0, 1],
                [1, 1, 0, 1],
            ],
        )
        mesh = Mesh(shader, attributes)
        self.add(mesh)

        self.wait(5)


class InteractiveDevelopment(Scene):
    def construct(self):
        circle = Circle()
        circle.set_fill(BLUE, opacity=0.5)
        circle.set_stroke(BLUE_E, width=4)
        square = Square()

        self.play(Create(square))
        self.wait()

        # This opens an iPython terminal where you can keep writing
        # lines as if they were part of this construct method.
        # In particular, 'square', 'circle' and 'self' will all be
        # part of the local namespace in that terminal.
        # self.embed()

        # Try copying and pasting some of the lines below into
        # the interactive shell
        self.play(ReplacementTransform(square, circle))
        self.wait()
        self.play(circle.animate.stretch(4, 0))
        self.play(Rotate(circle, 90 * DEGREES))
        self.play(circle.animate.shift(2 * RIGHT).scale(0.25))

        # text = Text(
        #     """
        #     In general, using the interactive shell
        #     is very helpful when developing new scenes
        # """
        # )
        # self.play(Write(text))

        # # In the interactive shell, you can just type
        # # play, add, remove, clear, wait, save_state and restore,
        # # instead of self.play, self.add, self.remove, etc.

        # # To interact with the window, type touch().  You can then
        # # scroll in the window, or zoom by holding down 'z' while scrolling,
        # # and change camera perspective by holding down 'd' while moving
        # # the mouse.  Press 'r' to reset to the standard camera position.
        # # Press 'q' to stop interacting with the window and go back to
        # # typing new commands into the shell.

        # # In principle you can customize a scene to be responsive to
        # # mouse and keyboard interactions
        # always(circle.move_to, self.mouse_point)


class SurfaceExample(Scene):
    def construct(self):
        # surface_text = Text("For 3d scenes, try using surfaces")
        # surface_text.fix_in_frame()
        # surface_text.to_edge(UP)
        # self.add(surface_text)
        # self.wait(0.1)

        torus1 = Torus(major_radius=1, minor_radius=1)
        torus2 = Torus(major_radius=3, minor_radius=1)
        sphere = Sphere(radius=3, resolution=torus1.resolution)
        # You can texture a surface with up to two images, which will
        # be interpreted as the side towards the light, and away from
        # the light.  These can be either urls, or paths to a local file
        # in whatever you've set as the image directory in
        # the custom_config.yml file

        script_location = Path(__file__).resolve().parent
        day_texture = (
            script_location / "assets" / "1280px-Whole_world_-_land_and_oceans.jpg"
        )
        night_texture = script_location / "assets" / "1280px-The_earth_at_night.jpg"

        surfaces = [
            OpenGLTexturedSurface(surface, day_texture, night_texture)
            for surface in [sphere, torus1, torus2]
        ]

        for mob in surfaces:
            mob.shift(IN)
            mob.mesh = OpenGLSurfaceMesh(mob)
            mob.mesh.set_stroke(BLUE, 1, opacity=0.5)

        # Set perspective
        frame = self.renderer.camera
        frame.set_euler_angles(
            theta=-30 * DEGREES,
            phi=70 * DEGREES,
        )

        surface = surfaces[0]

        self.play(
            FadeIn(surface),
            Create(surface.mesh, lag_ratio=0.01, run_time=3),
        )
        for mob in surfaces:
            mob.add(mob.mesh)
        surface.save_state()
        self.play(Rotate(surface, PI / 2), run_time=2)
        for mob in surfaces[1:]:
            mob.rotate(PI / 2)

        self.play(Transform(surface, surfaces[1]), run_time=3)

        self.play(
            Transform(surface, surfaces[2]),
            # Move camera frame during the transition
            frame.animate.increment_phi(-10 * DEGREES),
            frame.animate.increment_theta(-20 * DEGREES),
            run_time=3,
        )
        # Add ambient rotation
        frame.add_updater(lambda m, dt: m.increment_theta(-0.1 * dt))

        # Play around with where the light is
        # light_text = Text("You can move around the light source")
        # light_text.move_to(surface_text)
        # light_text.fix_in_frame()

        # self.play(FadeTransform(surface_text, light_text))
        light = self.camera.light_source
        self.add(light)
        light.save_state()
        self.play(light.animate.move_to(3 * IN), run_time=5)
        self.play(light.animate.shift(10 * OUT), run_time=5)

        # drag_text = Text("Try moving the mouse while pressing d or s")
        # drag_text.move_to(light_text)
        # drag_text.fix_in_frame()
