from manim_imports_ext import *


def box_blur(n):
    return np.ones((n, n)) / (n**2)


class ConvolutionIntroduction(ThreeDScene):
    def construct(self):
        frame = self.camera.frame

        # Setup the pixel grids
        image = Image.open(get_full_raster_image_path("Mario"))

        arr = np.array(image)
        arr = arr[8fc00:db20:35b:7399::5, 0::40]
        height, width = arr.shape[:2]

        pixel_array = VGroup(*[
            Square(fill_color=rgb_to_hex(arr[i, j] / 255), fill_opacity=1)
            for i in range(height)
            for j in range(width)
        ])
        pixel_array.arrange_in_grid(height, width, buff=0)
        pixel_array.set_height(6)
        pixel_array.set_stroke(WHITE, 1)
        pixel_array.to_edge(LEFT, buff=LARGE_BUFF)

        new_array = pixel_array.copy()
        new_array.next_to(pixel_array, RIGHT, buff=2)
        new_array.set_fill(BLACK, 0)

        self.add(pixel_array)
        self.add(new_array)

        # Setup kernel
        def get_kernel_array(kernel, pixel_array=pixel_array, tex=None):
            kernel_array = VGroup()
            for row in kernel:
                for x in row:
                    square = pixel_array[0].copy()
                    square.set_fill(BLACK, 0)
                    square.set_stroke(BLUE, 2)
                    if tex:
                        value = Tex(tex)
                    else:
                        value = DecimalNumber(x, num_decimal_places=3)
                    value.set_width(square.get_width() * 0.7)
                    value.set_stroke(BLACK, 1, background=True)
                    value.move_to(square)
                    square.add(value)
                    kernel_array.add(square)
            kernel_array.arrange_in_grid(*kernel.shape, buff=0)
            kernel_array.move_to(pixel_array[0])
            return kernel_array

        kernel = box_blur(3)
        kernel_array = get_kernel_array(kernel, tex="1 / 9")
        self.add(kernel_array)

        # Define step
        right_rect = new_array[0].copy()
        right_rect.set_stroke(BLUE, 2)
        self.add(right_rect)

        def step(pos=0):
            i = pos // width
            j = pos % width

            h, w = kernel.shape
            pixels = np.array([
                square.data["fill_rgba"][0]
                for square in pixel_array
            ]).reshape((height, width, 4))

            rgba = sum([
                kernel[k, l] * pixels[i - k, j - l]
                for k in range(-(w // 2), w // 2 + 1)
                for l in range(-(h // 2), h // 2 + 1)
                if (0 <= i - k < pixels.shape[0]) and (0 <= j - l < pixels.shape[1])
            ])

            kernel_array.move_to(pixel_array[pos])
            right_rect.move_to(new_array[pos])
            new_array[pos].data["fill_rgba"][0] = rgba

        def walk(start, stop, time=5, surface=None):
            for n in range(start, stop):
                step(n)
                if surface is not None:
                    surface.move_to(kernel_array, IN)
                self.wait(time / (stop - start))

        # Setup zooming
        def zoom_to_kernel():
            self.play(
                frame.set_height, 1.5 * kernel_array.get_height(),
                frame.move_to, kernel_array,
                run_time=2
            )

        def zoom_to_new_pixel():
            self.play(
                frame.set_height, 1.5 * kernel_array.get_height(),
                frame.move_to, right_rect,
                run_time=2
            )

        def reset_frame():
            self.play(
                frame.to_default_state
            )

        # Example walking
        # walk(0, 151, 15)
        last_i = 0
        next_i = 151
        walk(last_i, next_i, 5)
        self.wait()
        zoom_to_kernel()
        self.wait()
        reset_frame()
        zoom_to_new_pixel()
        self.wait()
        reset_frame()

        # last_i = next_i
        # next_i = 200
        # walk(151, 200, 2)
        # self.wait(0.5)
        # zoom_to_kernel()
        # self.wait()
        # reset_frame()
        # zoom_to_new_pixel()
        # self.wait()
        # reset_frame()

        last_i = next_i
        next_i = len(pixel_array)
        walk(last_i, next_i, 10)
        # self.wait()
        # zoom_to_kernel()
        # self.wait()
        # reset_frame()
        self.wait()

        # Gauss kernel
        gauss_kernel = np.array([
            [0.00296902, 0.0133062, 0.0219382, 0.0133062, .00296902],
            [0.0133062, 0.0596343, 0.0983203, 0.0596343, 0.0133062],
            [0.0219382, 0.0983203, 0.162103, 0.0983203, 0.0219382],
            [0.0133062, 0.0596343, 0.0983203, 0.0596343, 0.0133062],
            [0.00296902, 0.0133062, 0.0219382, 0.0133062, 0.00296902],
        ])  # Oh good, hard coded, I hope you feel happy with yourself.
        gauss_array = get_kernel_array(gauss_kernel)
        kernel_array.set_submobjects(gauss_array)

        kernel = gauss_kernel
        new_array.set_fill(BLACK, 0)

        walk(0, 200, time=5)
        # walk(0, 200, time=10)
        self.wait()
        zoom_to_kernel()
        self.wait()

        # Gauss surface
        gaussian = ParametricSurface(
            lambda u, v: [u, v, np.exp(-(u**2) - v**2)],
            u_range=(-3, 3),
            v_range=(-3, 3),
            resolution=(101, 101),
        )
        gaussian.set_color(BLUE, 0.8)
        gaussian.match_width(kernel_array)
        gaussian.stretch(2, 2)

        def update_surface(surface, kernel_array=kernel_array):
            surface.move_to(kernel_array, IN)

        update_surface(gaussian)
        self.play(
            FadeIn(gaussian),
            frame.set_phi, 70 * DEGREES,
            frame.set_theta, 10 * DEGREES,
            run_time=3
        )
        self.wait()
        self.play(
            frame.set_height, 8,
            frame.set_theta, 0,
            frame.set_x, 0,
            run_time=3,
        )

        # More walking
        walk(200, len(pixel_array), time=10, surface=gaussian)
        self.wait()
        self.play(frame.to_default_state, run_time=2)
        self.wait()

from manim_imports_ext import *


def box_blur(n):
    return np.ones((n, n)) / (n**2)


class ConvolutionIntroduction(ThreeDScene):
    def construct(self):
        frame = self.camera.frame

        # Setup the pixel grids
        image = Image.open(get_full_raster_image_path("Mario"))

        arr = np.array(image)
        arr = arr[8fc00:db20:35b:7399::5, 0::40]
        height, width = arr.shape[:2]

        pixel_array = VGroup(*[
            Square(fill_color=rgb_to_hex(arr[i, j] / 255), fill_opacity=1)
            for i in range(height)
            for j in range(width)
        ])
        pixel_array.arrange_in_grid(height, width, buff=0)
        pixel_array.set_height(6)
        pixel_array.set_stroke(WHITE, 1)
        pixel_array.to_edge(LEFT, buff=LARGE_BUFF)

        new_array = pixel_array.copy()
        new_array.next_to(pixel_array, RIGHT, buff=2)
        new_array.set_fill(BLACK, 0)

        self.add(pixel_array)
        self.add(new_array)

        # Setup kernel
        def get_kernel_array(kernel, pixel_array=pixel_array, tex=None):
            kernel_array = VGroup()
            for row in kernel:
                for x in row:
                    square = pixel_array[0].copy()
                    square.set_fill(BLACK, 0)
                    square.set_stroke(BLUE, 2)
                    if tex:
                        value = Tex(tex)
                    else:
                        value = DecimalNumber(x, num_decimal_places=3)
                    value.set_width(square.get_width() * 0.7)
                    value.set_stroke(BLACK, 1, background=True)
                    value.move_to(square)
                    square.add(value)
                    kernel_array.add(square)
            kernel_array.arrange_in_grid(*kernel.shape, buff=0)
            kernel_array.move_to(pixel_array[0])
            return kernel_array

        kernel = box_blur(3)
        kernel_array = get_kernel_array(kernel, tex="1 / 9")
        self.add(kernel_array)

        # Define step
        right_rect = new_array[0].copy()
        right_rect.set_stroke(BLUE, 2)
        self.add(right_rect)

        def step(pos=0):
            i = pos // width
            j = pos % width

            h, w = kernel.shape
            pixels = np.array([
                square.data["fill_rgba"][0]
                for square in pixel_array
            ]).reshape((height, width, 4))

            rgba = sum([
                kernel[k, l] * pixels[i - k, j - l]
                for k in range(-(w // 2), w // 2 + 1)
                for l in range(-(h // 2), h // 2 + 1)
                if (0 <= i - k < pixels.shape[0]) and (0 <= j - l < pixels.shape[1])
            ])

            kernel_array.move_to(pixel_array[pos])
            right_rect.move_to(new_array[pos])
            new_array[pos].data["fill_rgba"][0] = rgba

        def walk(start, stop, time=5, surface=None):
            for n in range(start, stop):
                step(n)
                if surface is not None:
                    surface.move_to(kernel_array, IN)
                self.wait(time / (stop - start))

        # Setup zooming
        def zoom_to_kernel():
            self.play(
                frame.set_height, 1.5 * kernel_array.get_height(),
                frame.move_to, kernel_array,
                run_time=2
            )

        def zoom_to_new_pixel():
            self.play(
                frame.set_height, 1.5 * kernel_array.get_height(),
                frame.move_to, right_rect,
                run_time=2
            )

        def reset_frame():
            self.play(
                frame.to_default_state
            )

        # Example walking
        # walk(0, 151, 15)
        last_i = 0
        next_i = 151
        walk(last_i, next_i, 5)
        self.wait()
        zoom_to_kernel()
        self.wait()
        reset_frame()
        zoom_to_new_pixel()
        self.wait()
        reset_frame()

        # last_i = next_i
        # next_i = 200
        # walk(151, 200, 2)
        # self.wait(0.5)
        # zoom_to_kernel()
        # self.wait()
        # reset_frame()
        # zoom_to_new_pixel()
        # self.wait()
        # reset_frame()

        last_i = next_i
        next_i = len(pixel_array)
        walk(last_i, next_i, 10)
        # self.wait()
        # zoom_to_kernel()
        # self.wait()
        # reset_frame()
        self.wait()

        # Gauss kernel
        gauss_kernel = np.array([
            [0.00296902, 0.0133062, 0.0219382, 0.0133062, .00296902],
            [0.0133062, 0.0596343, 0.0983203, 0.0596343, 0.0133062],
            [0.0219382, 0.0983203, 0.162103, 0.0983203, 0.0219382],
            [0.0133062, 0.0596343, 0.0983203, 0.0596343, 0.0133062],
            [0.00296902, 0.0133062, 0.0219382, 0.0133062, 0.00296902],
        ])  # Oh good, hard coded, I hope you feel happy with yourself.
        gauss_array = get_kernel_array(gauss_kernel)
        kernel_array.set_submobjects(gauss_array)

        kernel = gauss_kernel
        new_array.set_fill(BLACK, 0)

        walk(0, 200, time=5)
        # walk(0, 200, time=10)
        self.wait()
        zoom_to_kernel()
        self.wait()

        # Gauss surface
        gaussian = ParametricSurface(
            lambda u, v: [u, v, np.exp(-(u**2) - v**2)],
            u_range=(-3, 3),
            v_range=(-3, 3),
            resolution=(101, 101),
        )
        gaussian.set_color(BLUE, 0.8)
        gaussian.match_width(kernel_array)
        gaussian.stretch(2, 2)

        def update_surface(surface, kernel_array=kernel_array):
            surface.move_to(kernel_array, IN)

        update_surface(gaussian)
        self.play(
            FadeIn(gaussian),
            frame.set_phi, 70 * DEGREES,
            frame.set_theta, 10 * DEGREES,
            run_time=3
        )
        self.wait()
        self.play(
            frame.set_height, 8,
            frame.set_theta, 0,
            frame.set_x, 0,
            run_time=3,
        )

        # More walking
        walk(200, len(pixel_array), time=10, surface=gaussian)
        self.wait()
        self.play(frame.to_default_state, run_time=2)
        self.wait()

# Setup the pixel grids # Setup kernel # Define step # Setup zooming # Example walking # walk(0, 151, 15) # last_i = next_i # next_i = 200 # walk(151, 200, 2) # self.wait(0.5) # zoom_to_kernel() # self.wait() # reset_frame() # zoom_to_new_pixel() # self.wait() # reset_frame() # self.wait() # zoom_to_kernel() # self.wait() # reset_frame() # Gauss kernel # Oh good, hard coded, I hope you feel happy with yourself. # walk(0, 200, time=10) # Gauss surface # More walking