# I found this example for PyCuda here:
# http://wiki.tiker.net/PyCuda/Examples/Mandelbrot
#
# An improved sequential/pure Python code was contributed
# by CRVSADER//KY <crusaderky@gmail.com>.
#
# I adapted it for PyOpenCL. Hopefully it is useful to someone.
# July 2010, HolgerRapp@gmx.net
#
# Original readme below these lines.

# Mandelbrot calculate using GPU, Serial numpy and faster numpy
# Use to show the speed difference between CPU and GPU calculations
# ian@ianozsvald.com March 2010

# Based on vegaseat's TKinter/numpy example code from 2006
# http://www.daniweb.com/code/snippet216851.html#
# with minor changes to move to numpy from the obsolete Numeric

import time

import numpy as np
from PIL import Image

import pyopencl as cl


# You can choose a calculation routine below (calc_fractal), uncomment
# one of the three lines to test the three variations
# Speed notes are listed in the same place

# set width and height of window, more pixels take longer to calculate
w = 2048
h = 2048


def calc_fractal_opencl(q, maxiter):
    ctx = cl.create_some_context()
    queue = cl.CommandQueue(ctx)

    output = np.empty(q.shape, dtype=np.uint16)

    mf = cl.mem_flags
    q_opencl = cl.Buffer(ctx, mf.READ_ONLY | mf.COPY_HOST_PTR, hostbuf=q)
    output_opencl = cl.Buffer(ctx, mf.WRITE_ONLY, output.nbytes)

    prg = cl.Program(
        ctx,
        """
    #pragma OPENCL EXTENSION cl_khr_byte_addressable_store : enable
    __kernel void mandelbrot(__global float2 *q,
                     __global ushort *output, ushort const maxiter)
    {
        int gid = get_global_id(0);
        float nreal, real = 0;
        float imag = 0;

        output[gid] = 0;

        for(int curiter = 0; curiter < maxiter; curiter++) {
            nreal = real*real - imag*imag + q[gid].x;
            imag = 2* real*imag + q[gid].y;
            real = nreal;

            if (real*real + imag*imag > 4.0f) {
                 output[gid] = curiter;
                 break;
            }
        }
    }
    """,
    ).build()

    prg.mandelbrot(
        queue, output.shape, None, q_opencl, output_opencl, np.uint16(maxiter)
    )

    cl.enqueue_copy(queue, output, output_opencl).wait()

    return output


def calc_fractal_serial(q, maxiter):
    # calculate z using pure python on a numpy array
    # note that, unlike the other two implementations,
    # the number of iterations per point is NOT constant
    z = np.zeros(q.shape, complex)
    output = np.resize(
        np.array(
            0,
        ),
        q.shape,
    )
    for i in range(len(q)):
        for iter in range(maxiter):
            z[i] = z[i] * z[i] + q[i]
            if abs(z[i]) > 2.0:
                output[i] = iter
                break
    return output


def calc_fractal_numpy(q, maxiter):
    # calculate z using numpy, this is the original
    # routine from vegaseat's URL
    output = np.resize(
        np.array(
            0,
        ),
        q.shape,
    )
    z = np.zeros(q.shape, np.complex64)

    for it in range(maxiter):
        z = z * z + q
        done = np.greater(abs(z), 2.0)
        q = np.where(done, 0 + 0j, q)
        z = np.where(done, 0 + 0j, z)
        output = np.where(done, it, output)
    return output


# choose your calculation routine here by uncommenting one of the options
calc_fractal = calc_fractal_opencl
# calc_fractal = calc_fractal_serial
# calc_fractal = calc_fractal_numpy


class Mandelbrot:
    def draw(self, x1, x2, y1, y2, maxiter=30):
        # draw the Mandelbrot set, from numpy example
        xx = np.arange(x1, x2, (x2 - x1) / w)
        yy = np.arange(y2, y1, (y1 - y2) / h) * 1j
        q = np.ravel(xx + yy[:, np.newaxis]).astype(np.complex64)

        start_main = time.time()
        output = calc_fractal(q, maxiter)
        end_main = time.time()

        secs = end_main - start_main
        print("Main took", secs)

        self.mandel = (output.reshape((h, w)) / float(output.max()) * 255.0).astype(
            np.uint8
        )

    def create_image(self):
        """ "
        create the image from the draw() string
        """
        # you can experiment with these x and y ranges
        self.draw(-2.13, 0.77, -1.3, 1.3)
        self.im = Image.fromarray(self.mandel)
        self.im.putpalette([i for rgb in ((j, 0, 0) for j in range(255))
            for i in rgb])

    def create_label(self):
        # put the image on a label widget
        self.image = ImageTk.PhotoImage(self.im)
        self.label = tk.Label(self.root, image=self.image)
        self.label.pack()

    def run_tk(self):
        self.root = tk.Tk()
        self.root.title("Mandelbrot Set")
        self.create_image()
        self.create_label()
        # start event loop
        self.root.mainloop()


if __name__ == "__main__":
    test = Mandelbrot()
    try:
        import tkinter as tk
    except ModuleNotFoundError:
        test.create_image()
    else:
        from PIL import ImageTk
        try:
            test.run_tk()
        except tk.TclError:
            test.create_image()
