'''
Classes and functions having to do with color.
'''

import morpholib as morpho
from morpholib import numTween
from morpholib.tools.basics import listfloor, listceil

import math, cairo
import numpy as np
from numbers import Number
import colorsys

# Normalizes an RGB triplet in the range [0...255] into
# the range [0.0, 1.0].
# RGB can be specified as three separate inputs, or as a
# tuple supplied as one input.
# Optional upperbound argument can be supplied to change
# what the max possible value for the input RGB values is.
# Default: 255.
def rgbNormalize(R, G=None, B=None, upperbound=255):
    # If one of the components is missing,
    # assume user supplied RGB as a tuple in the first input.
    if G is None or B is None:
        R,G,B = R
    return (R/upperbound, G/upperbound, B/upperbound)

# Checks that a string codes to a valid hex color
# That is, consists only of the characters "0" thru "f"
# Returns boolean indicating validity.
def validHexColor(string):
    string = string.lower()
    if string[:2] == "0x":
        string = string[2:]
    if len(string) == 0:
        return False
    for char in string:
        if char not in "0123456789abcdef":
            return False
    return True

# Parses a hex color string into an RGB triple (0-1, 0-1, 0-1)
# If optional argument normalize is set to False, the RGB triple returned
# is the raw hex data in the range [0...255]: (0-255, 0-255, 0-255).
# By default: normalize = True.
def parseHexColor(string, normalize=True):
    if isinstance(string, str):
        # if not validHexColor(string):
        #     raise ValueError("Not a valid hex string!")
        N = int(string, 16)
    else:
        N = int(string)

    B = N % 256

    N = N // 256
    G = N % 256

    N = N // 256
    R = N % 256

    if normalize:
        return tuple(X/255 for X in (R,G,B))
    else:
        return (R,G,B)

# Converts RGB triple into HSV triple.
# Both input and output triples have components in the range [0,1].
def rgb2hsv(rgb):
    if isinstance(rgb, str) or isinstance(rgb, Number):
        rgb = parseHexColor(rgb)

    return colorsys.rgb_to_hsv(*rgb)

# Converts HSV triple into RGB triple.
# Both input and output triples have components in the range [0,1].
def hsv2rgb(hsv):
    if isinstance(hsv, str) or isinstance(hsv, Number):
        hsv = parseHexColor(hsv)

    return colorsys.hsv_to_rgb(*hsv)


# Takes a color string (whether named or hex) and returns
# the normalized RGB triple (0-1, 0-1, 0-1)
def colorStr2Tuple(string):
    string = string.strip().lower()
    if string in colormap:
        return colormap[string]
    else:
        return rgbNormalize(parseHexColor(string))

# Same as colorStr2Tuple but returns a list instead of a tuple.
def colorStr2List(string):
    return list(colorStr2Tuple(string))

# Dict of named colors
colormap = {
    "black": (0,0,0),
    "white": (1,1,1),
    "red": (1,0,0),
    "green": rgbNormalize(0x23, 0xb5, 0x83),
    "blue": (0,0,1),
    "yellow": (1,1,0),
    "magenta": (1,0,1),
    "cyan": (0,1,1),
    "orange": rgbNormalize(0xff, 0xa5, 0),
    "violet": parseHexColor("800080"),
    "brown": rgbNormalize(0x80, 0x40, 0)
}

# List of the colors in proper order.
# For use in constructing the combobox ONLY!
# colormap should be used for programming purposes.
colors = ["red", "green", "blue", "yellow", "cyan", "magenta",
    "orange", "violet", "brown", "white", "black"]

# Transparently overlays the color A over the color B
# using transparency alpha (default=0.5)
def alphaOverlay(A, B, alpha=0.5):
    if type(A) is list or type(A) is tuple:
        result = [0,0,0]
        for i in range(len(A)):
            result[i] = alphaOverlay(A[i], B[i], alpha)
        return tuple(result)
    else:
        return A*alpha + B*(1-alpha)

# Tweens two RGB colors represented by triples and returns
# a new list with the tweened value.
def colorTween(rgb1, rgb2, t, start=0, end=1):
    if rgb1 == rgb2: return rgb1

    R1, G1, B1 = rgb1
    R2, G2, B2 = rgb2

    dR = R2 - R1
    dG = G2 - G1
    dB = B2 - B1

    R = R1 + t*dR
    G = G1 + t*dG
    B = B1 + t*dB

    R = numTween(R1, R2, t, start, end)
    G = numTween(G1, G2, t, start, end)
    B = numTween(B1, B2, t, start, end)

    return [R,G,B]



# Provides a datatype in which you can specify gradients like color
# gradients or variable width paths. It inherits off of Figure, but
# it's not really supposed to function like a figure. There's no draw()
# method, for example. But making it a figure allows it to possess
# tween methods, which will be helpful for any classes that want to use
# gradients so that they don't have to worry about how to do the tweening
# themselves.
#
# Usage: Gradient(data) where "data" is a dict mapping numbers in the
# unit interval [0,1] to color values.
class Gradient(morpho.Figure):
    def __init__(self, data=None):
        if data is None:
            data = {}
        elif isinstance(data, list) or isinstance(data, tuple):
            data = {0: data}
        elif isinstance(data, float) or isinstance(data, int):
            data = {0: data}
        elif not isinstance(data, dict):
            raise TypeError("Unsupported type for data supplied to Gradient()")


        super().__init__()

        # Define tweenables

        # data is a dict that maps floats in the range [0,1]
        # to gradient waypoint values (e.g. RGB, floats, etc.)
        data = morpho.Tweenable("data", data, tags=["gradient", "nolinear", "nospiral"])
        self.update([data])

        # Set to true to identify 0 with 1 and make the gradient looped.
        # For now this is not implemented and does nothing.
        self.cycle = False


    def copy(self):
        # Copy according to superclass first
        new = super().copy()

        # Go thru the data dict and make copies of each individual value (if needed)
        for key in new.data:
            value = new.data[key]
            if "copy" in dir(value):
                new.data[key] = value.copy()

        # Copy the non-tweenable attributes manually
        new.cycle = self.cycle

        return new


    def __getitem__(self, key):
        return self.data[key]

    def __setitem__(self, key, value):
        self.data[key] = value

    def __len__(self):
        return len(self.data)

    # Returns the interpolated gradient value at the specified x in [0,1]
    def value(self, x):
        if len(self.data) == 0:
            raise KeyError("This gradient has no keys. Cannot interpolate an empty gradient.")

        keylist = sorted(list(self.data.keys()))

        x = float(x)  # Convert to python float

        # If the given parameter is a key in the dict, just return the value
        if x in keylist:
            return self[x]

        # Compute the latest key
        k = listfloor(keylist, x)
        if k == -1: return self[keylist[0]]
        key = keylist[k]

        # Grab latest value
        keyval = self.data[key]

        if key == keylist[-1]:
            return keyval
        else:
            key2 = keylist[k+1]
            keyval2 = self[key2]
            if isinstance(keyval, list) or isinstance(keyval, tuple):
                return type(keyval)(map(morpho.numTween, keyval, keyval2, ((x-key)/(key2-key),)*len(keyval)))
            else:
                return morpho.numTween(keyval, keyval2, x, start=key, end=key2)

    # Generates a new Gradient which is a segment between the values [a,b] of the
    # current gradient.
    def segment(self, a, b=None):
        if len(self.data) == 0:
            return Gradient()

        keylist = sorted(list(self.data.keys()))
        if b is None:
            b = keylist[-1]

        segdata = {}
        segdata[a] = self.value(a)
        start = listceil(keylist, a)
        end = listfloor(keylist, b)
        for x in keylist[start:end+1]:
            segdata[x] = self.data[x]
        segdata[b] = self.value(b)

        return Gradient(segdata).copy()  # To handle needing to make copies of lists, etc.

    # Normalizes the gradient parameter space so that the lowest parameter is 0
    # and the highest parameter is 1. Does nothing if the gradient has fewer than 2
    # nodes. This function also affects the gradient IN PLACE!
    def normalize(self):
        if len(self.data) < 2:
            return
        newdata = {}
        start = min(self.data)
        end = max(self.data)
        for x in self.data.keys():
            newdata[morpho.numTween(0, 1, x, start, end)] = self.data[x]
        self.data = newdata

    def verify(self):
        for key in self.data:
            if not(0 <= key <= 1):
                raise ValueError("Gradient error contains key values not in the range [0,1]")

    def __repr__(self):
        if len(self.data) == 0:
            return "Gradient()"

        keys = list(self.data.keys())
        lines = []
        for key in keys:
            lines.append(repr(key) + ": " + repr(self.data[key]))

        return "Gradient({\n    " + ",\n    ".join(lines) + "\n})"


    @morpho.TweenMethod
    def tweenLinear(self, other, t):
        # # Defaults to the empty tuple.
        # if ignore is None:
        #     ignore = ()
        # # Convert string to tuple containing string
        # elif isinstance(ignore, str):
        #     ignore = (ignore,)

        # if "data" in ignore:
        #     return self.copy()

        self.verify()
        other.verify()

        if len(self.data) > len(other.data):
            # Tweened figure inherits basic attributes of self,
            # but gets its state determined by the following code.
            tw = self.copy()
            tw.update(other.tweenLinear(self, 1-t)._state)
            return tw
        elif len(self.data) < len(other.data):
            numNewNodes = len(other.data) - len(self.data)
            M = numNewNodes + 1

            # If self is empty, just return copy of other.
            if len(self.data) == 0:
                return other.copy()

            # Make a copy of self and populate it with more nodes
            newself = self.copy()

            # How many new nodes need to be added?

            # First get a list of nodes (and include 0 and 1 if not)
            nodes = sorted(list(self.data.keys()))
            if nodes[0] != 0:
                nodes.insert(0, 0)
            if nodes[-1] != 1:
                nodes.append(1)

            # Iterate over the gaps, indexed by left endpoint
            for n in range(len(nodes)-1):
                # Identify endpoints and number N of new nodes
                # to sprinkle in this gap.
                a = nodes[n]
                b = nodes[n+1]
                N = math.ceil(M*b) - math.ceil(max(M*a, 1))

                # Sprinkle new nodes in uniformly local to the gap
                gapSize = b - a
                for k in range(1, N+1):
                    newNode = a + k/(N+1)*gapSize
                    newself.data[newNode] = self.value(newNode)

            # Temporary for testing! Remove this at some point!
            assert len(newself.data) == len(other.data)

            return newself.tweenLinear(other, t)

        # From this point, we assume the dict lengths are the same.

        # Get keys in sorted order
        selfkeys = sorted(list(self.data.keys()))
        otherkeys = sorted(list(other.data.keys()))
        newkeys = list(map(morpho.numTween, selfkeys, otherkeys, (t,)*len(selfkeys)))

        selfvals = [self.data[selfkeys[n]] for n in range(len(selfkeys))]
        othervals = [other.data[otherkeys[n]] for n in range(len(otherkeys))]

        if isinstance(selfvals[0], list) or isinstance(selfvals[0], tuple):
            newvals = []
            for n in range(len(selfvals)):
                selfval = selfvals[n]
                otherval = othervals[n]
                newvals.append(type(selfval)(map(morpho.numTween, selfval, otherval, (t,)*len(selfval))))
        else:
            newvals = list(map(morpho.numTween, selfvals, othervals, (t,)*len(selfvals)))

        # Construct updated data dict
        data = {}
        for n in range(len(newkeys)):
            data[newkeys[n]] = newvals[n]

        # Construct tweened Gradient figure
        tw = type(self)(data)  # Using type(self) instead of explicit Gradient() for sake of possible inheritance
        tw.cycle = self.cycle

        return tw

    # For this figure, spiral tween and linear tween are identical
    tweenSpiral = tweenLinear

    # Pivot tween is not supported for gradients
    def tweenPivot(self, other, t):
        raise NotImplementedError


# Decorator modifies tween methods to support
# gradients for the specified tweenables.
#
# Example usage:
# @morpho.TweenMethod
# @handleGradients(["color", "fill", "alpha"])
# def tweenLinear(self, other, t):
#     etc.
def handleGradients(gradTweenableNames):
    def decorator(twMethod):
        def wrapper(self, other, t, *args, **kwargs):
            # Use the original tween method as normal.
            # The given tween method should be designed to ignore
            # the tweenables that may contain gradients.
            tw = twMethod(self, other, t, *args, **kwargs)

            for name in gradTweenableNames:
                # If both are gradients...
                if isinstance(self._state[name].value, Gradient) and isinstance(other._state[name].value, Gradient):
                    # tw.color = self.color.tweenLinear(other.color, t)
                    tw._state[name].value = self._state[name].value.tweenLinear(other._state[name].value, t)
                # self is gradient but not other
                elif isinstance(self._state[name].value, Gradient):
                    othercopy_state_name = self._state[name].value.copy()
                    for key in othercopy_state_name.data:
                        value = other._state[name].value
                        othercopy_state_name[key] = value.copy() if "copy" in dir(value) else value
                    tw._state[name].value = self._state[name].value.tweenLinear(othercopy_state_name, t)
                # other is gradient but not self
                elif isinstance(other._state[name].value, Gradient):
                    selfcopy_state_name = other._state[name].value.copy()
                    for key in selfcopy_state_name.data:
                        value = self._state[name].value
                        selfcopy_state_name[key] = value.copy() if "copy" in dir(value) else value
                    tw._state[name].value = selfcopy_state_name.tweenLinear(other._state[name].value, t)
                # neither are gradients
                else:
                    value = self._state[name].value
                    if isinstance(value, list) or isinstance(value, tuple):
                        tw._state[name].value = type(value)(map(morpho.numTween, self._state[name].value, other._state[name].value, (t,)*len(value)))
                    elif isinstance(value, np.ndarray):
                        tw._state[name].value = morpho.numTween1(self._state[name].value, other._state[name].value, t)
                    else:
                        tw._state[name].value = morpho.numTween(self._state[name].value, other._state[name].value, t)

            return tw
        return wrapper
    return decorator


# This class does nothing (by itself). It just exists to be the parent of all
# GradientFill subclasses like LinearGradientFill and RadialGradientFill.
# It makes it easier to implement handleGradientFills because I can use
# isinstance(fig, GradientFill) to check for being an object of ANY
# GradientFill subclass.
class GradientFill(morpho.Figure):
    # This method should be tailored to each subclass. It should generate
    # a cairo source object (like gradient or mesh pattern). It is also
    # expected that this method will implicitly call
    # ctx.save() via morpho.pushPhysicalCoords(), so you will always need
    # to call ctx.restore() at some later point after calling this method.
    def makeSource(self, camera, ctx, alpha=1, pushPhysicalCoords=True):
        if pushPhysicalCoords:
            morpho.pushPhysicalCoords(camera.view, ctx)  # Implicit ctx.save()
        return None

    # This method generates the correct gradient source object (after
    # pushing physical coordinates) and then calls
    # fill_preserve() on the given cairo context.
    # It then calls ctx.restore() as a final step, so the
    # transformation stack should be unchanged after this method is
    # finished.
    def draw(self, camera, ctx, alpha=1, pushPhysicalCoords=True, *, strokeToo=False):
        source = self.makeSource(camera, ctx, alpha, pushPhysicalCoords)  # Implicitly calls ctx.save()
        ctx.set_source(source)
        ctx.fill_preserve()
        if strokeToo:
            ctx.stroke_preserve()
        if pushPhysicalCoords:
            ctx.restore()

# Non-drawable figure that describes how to fill a shape with a linear
# gradient.
# "tail" and "head" represent where (in physical space) the gradient should
# begin and end (tail: z=0, and head: z=1). Default to tail=0, head=1.
# "gradient" is the Gradient figure used. Defaults to the blank Gradient.
# "origin" describes where in physical space the origin of gradient space is
# in the event that you want gradient physical coordinates to be offset
# from the standard physical coordinates. Defaults to origin=0 so that
# gradient space and physical space coincide.
# Basically, origin is just a translation parameter that can be added to
# head and tail by classes that use GradientFill.
# Please note that the expression "self.head" and "self.tail" will NOT
# automatically incorporate the origin offset. "origin" is just data,
# and it is the responsibility of classes that use GradientFill to make
# use of the origin attribute if they so choose.
# Correct usage: PHYSICAL tail = self.origin + self.tail
class LinearGradientFill(GradientFill):
    def __init__(self, tail=0, head=1, gradient=None, origin=0):
        if gradient is None:
            gradient = Gradient()

        # Use superclass constructor
        super().__init__()

        # Tweenables
        tail = morpho.Tweenable("tail", tail, tags=["complex", "tail"])
        head = morpho.Tweenable("head", head, tags=["complex", "head"])
        gradient = morpho.Tweenable("gradient", gradient, tags=["gradient", "nolinear", "nospiral"])
        origin = morpho.Tweenable("origin", origin, tags=["complex"])

        self.update([tail, head, gradient, origin])

    # Returns a cairo gradient object which can be set as a cairo source to draw
    # a gradient somewhere
    def makeSource(self, camera, ctx, alpha=1, pushPhysicalCoords=True):
        if pushPhysicalCoords:
            morpho.pushPhysicalCoords(camera.view, ctx)  # Implicit ctx.save()

        # X0, Y0 = morpho.screenCoords(self.fill.tail+self.fill.origin, view, ctx)
        # X1, Y1 = morpho.screenCoords(self.fill.head+self.fill.origin, view, ctx)
        # cairoGrad = cairo.LinearGradient(X0,Y0, X1,Y1)
        z0 = self.tail + self.origin
        z1 = self.head + self.origin
        cairoGrad = cairo.LinearGradient(z0.real, z0.imag, z1.real, z1.imag)
        # If the color gradient is an RGBA gradient,
        # use the alpha values accordingly.
        if len(list(self.gradient.data.values())[0]) == 4:
            for key in self.gradient.data:
                R,G,B,A = self.gradient[key]
                cairoGrad.add_color_stop_rgba(key, R,G,B, A*alpha)
        else:
            for key in self.gradient.data:
                cairoGrad.add_color_stop_rgba(key, *self.gradient[key], alpha)
        # ctx.set_source(cairoGrad)
        # ctx.fill_preserve()
        return cairoGrad

    # makeSource = makeGradient

    # # Fills whatever the current path of the given cairo context is with the
    # # gradient specified by this figure.
    # def fillCurrentPath(self, camera, ctx):
    #     raise NotImplementedError

    #     cairoGrad = self.makeGradient(camera, ctx)
    #     ctx.set_source(cairoGrad)
    #     ctx.fill_preserve()

    @morpho.TweenMethod
    @handleGradients(["gradient"])
    def tweenLinear(self, other, t, *args, **kwargs):
        return super().tweenLinear(other, t, *args, **kwargs)


# Non-drawable figure that describes how to draw a radial gradient fill.
# "center" is a position (complex number) in physical space where the
# gradient is centered.
# r0, r1 denote the inner and outer physical radii of the gradient respectively.
# "gradient" is the Gradient figure used. Defaults to the blank Gradient.
# "origin" describes where in physical space the origin of gradient space is
# in the event that you want gradient physical coordinates to be offset
# from the standard physical coordinates. Defaults to origin=0 so that
# gradient space and physical space coincide.
# Basically, origin is just a translation parameter that can be added to
# the "center" tweenable by classes that use GradientFill.
class RadialGradientFill(GradientFill):
    def __init__(self, center=0, r0=0, r1=1, gradient=None, origin=0):
        if gradient is None:
            gradient = Gradient()

        # Use superclass constructor
        super().__init__()

        # Tweenables
        center = morpho.Tweenable("center", center, tags=["complex", "position"])
        r0 = morpho.Tweenable("r0", r0, tags=["scalar"])
        r1 = morpho.Tweenable("r1", r1, tags=["scalar"])
        gradient = morpho.Tweenable("gradient", gradient, tags=["gradient", "nolinear", "nospiral"])
        origin = morpho.Tweenable("origin", origin, tags=["complex"])

        self.update([center, r0, r1, gradient, origin])

    # Returns a cairo gradient object which can be set as a cairo source to draw
    # a gradient somewhere.
    # PLEASE NOTE: This method implicitly calls ctx.save(), so you will
    # need to call ctx.restore() later to reset it.
    def makeSource(self, camera, ctx, alpha=1, pushPhysicalCoords=True):
        if pushPhysicalCoords:
            morpho.pushPhysicalCoords(camera.view, ctx)  # Implicit ctx.save()

        # X0, Y0 = morpho.screenCoords(self.fill.center+self.fill.origin, view, ctx)
        # R0 = morpho.pixelWidth(self.fill.r0, view, ctx)
        # R1 = morpho.pixelWidth(self.fill.r1, view, ctx)

        z0 = self.center + self.origin
        cairoGrad = cairo.RadialGradient(
            z0.real,z0.imag, self.r0, z0.real,z0.imag, self.r1
            )
        # If the color gradient is an RGBA gradient,
        # use the alpha values accordingly.
        if len(list(self.gradient.data.values())[0]) == 4:
            for key in self.gradient.data:
                R,G,B,A = self.gradient[key]
                cairoGrad.add_color_stop_rgba(key, R,G,B, A*alpha)
        else:
            for key in self.gradient.data:
                cairoGrad.add_color_stop_rgba(key, *self.gradient[key], alpha)

        return cairoGrad
        # ctx.set_source(cairoGrad)
        # ctx.fill_preserve()

    # makeSource = makeGradient

    # # Fills whatever the current path of the given cairo context is with the
    # # gradient specified by this figure.
    # def fillCurrentPath(self, camera, ctx):
    #     raise NotImplementedError

    #     cairoGrad = self.makeGradient(camera, ctx)
    #     ctx.set_source(cairoGrad)
    #     ctx.fill_preserve()

    @morpho.TweenMethod
    @handleGradients(["gradient"])
    def tweenLinear(self, other, t, *args, **kwargs):
        return super().tweenLinear(other, t, *args, **kwargs)

# Base class for QuadGradientFill and QuadGradGroup
class QuadGrad(GradientFill):
    pass

class QuadGradientFill(QuadGrad):
    def __init__(self, vertices=None, colors=None, origin=0):

        super().__init__()

        # Tweenables
        if vertices is None:
            vertices = [0, 1, 1+1j, 1j]
        if colors is None:
            colors = [(1,0,0)]*4
        # Make sure colors is a list of lists, and not other vector type.
        colors = [list(rgb) for rgb in colors]

        vertices = morpho.Tweenable("vertices", vertices, tags=["complex", "list"])
        colors = morpho.Tweenable("colors", colors, tags=["color", "list"])
        origin = morpho.Tweenable("origin", origin, tags=["complex"])

        self.update([vertices, colors, origin])

    def copy(self):
        new = super().copy()

        # Do a deep copy of colors
        new.colors = [list(rgb) for rgb in self.colors]

        return new

    # Generate the cairo MeshPattern this QuadGradientFill describes.
    # Please note this method changes the coordinate system of the
    # inputted cairo context, and you will need to call ctx.restore()
    # at some point to reset it.
    def makeSource(self, camera, ctx, alpha=1, pushPhysicalCoords=True):
        if pushPhysicalCoords:
            morpho.pushPhysicalCoords(camera.view, ctx)  # Implicit ctx.save()

        pat = cairo.MeshPattern()
        # pat.begin_patch()
        # z0 = self.vertices[0]
        # x0,y0 = z0.real, z0.imag
        # pat.move_to(x0, y0)
        # for n in range(1,4):
        #     z = self.vertices[n] + self.origin
        #     pat.line_to(z.real, z.imag)
        # for n in range(0,4):
        #     color = self.colors[n]
        #     # Append alpha = 1 if color is RGB instead of RGBA
        #     if len(color) < 4:
        #         pat.set_corner_color_rgba(n, *color, alpha)
        #     else:
        #         pat.set_corner_color_rgba(n, *color[:3], alpha*color[3])
        # pat.end_patch()
        self.makePatch(pat, alpha)

        return pat

    # # Also, please remember calling this
    # # method implicitly calls ctx.save() so you will need to call
    # # ctx.restore() later.
    # makeSource = makeMeshPattern

    def makePatch(self, meshPattern, alpha=1):
        pat = meshPattern
        pat.begin_patch()
        z0 = self.vertices[0] + self.origin
        x0,y0 = z0.real, z0.imag
        pat.move_to(x0, y0)
        for n in range(1,4):
            z = self.vertices[n] + self.origin
            pat.line_to(z.real, z.imag)
        for n in range(0,4):
            color = self.colors[n]
            # Append alpha = 1 if color is RGB instead of RGBA
            if len(color) < 4:
                pat.set_corner_color_rgba(n, *color, alpha)
            else:
                pat.set_corner_color_rgba(n, *color[:3], alpha*color[3])
        pat.end_patch()


class QuadGradGroup(QuadGrad):
    def __init__(self, quadgrads=None):

        super().__init__()

        if quadgrads is None:
            quadgrads = []

        quadgrads = morpho.Tweenable("quadgrads", quadgrads, tags=["quadgradientfill", "list"])

        self.update([quadgrads])

    def copy(self):
        new = super().copy()

        # Do a deep copy of quadgrads list
        new.quadgrads = [quadgrad.copy() for quadgrad in self.quadgrads]

        return new

    # Generate the cairo MeshPattern this QuadGradientFill describes.
    # Please note this method changes the coordinate system of the
    # inputted cairo context, and you will need to call ctx.restore()
    # at some point to reset it.
    def makeSource(self, camera, ctx, alpha=1, pushPhysicalCoords=True):
        if pushPhysicalCoords:
            morpho.pushPhysicalCoords(camera.view, ctx)  # Implicit ctx.save()

        pat = cairo.MeshPattern()
        for quadgrad in self.quadgrads:
            quadgrad.makePatch(pat, alpha)

        return pat

    # makeSource = makeMeshPattern

    ### TWEEN METHODS ###

    @morpho.TweenMethod
    def tweenLinear(self, other, t):
        # # Defaults to the empty tuple.
        # if ignore is None:
        #     ignore = ()
        # # Convert string to tuple containing string
        # elif isinstance(ignore, str):
        #     ignore = (ignore,)

        # if "quadgrads" in ignore:
        #     return self.copy()

        # Initialize tweened object to a generic QuadGradGroup
        tw = type(self)()

        # Cycle thru all quadgrads in both self and other
        # (assuming they both have the same number)
        # and tween each one in self with the corresponding one in other.
        quadgrads = []
        for n in range(len(self.quadgrads)):
            a = self.quadgrads[n]
            b = other.quadgrads[n]
            quadgrads.append(a.defaultTween(a,b,t))

        tw.quadgrads = quadgrads
        return tw

    # Pivot tween is not supported for this figure
    def tweenPivot(self, other, t):
        raise NotImplementedError

# Decorator modifies tween methods to support
# gradient fills for the specified tweenables.
#
# Example usage:
# @morpho.TweenMethod
# @handleGradientFills(["color", "fill", "alpha"])
# def tweenLinear(self, other, t):
#     etc.
def handleGradientFills(gradFillTweenableNames):
    def decorator(twMethod):
        def wrapper(self, other, t, *args, **kwargs):
            # Use the original tween method as normal.
            # The given tween method should be designed to ignore
            # the tweenables that may contain gradients.
            tw = twMethod(self, other, t, *args, **kwargs)

            for name in gradFillTweenableNames:
                # If both are gradient fills...
                if isinstance(self._state[name].value, GradientFill) and isinstance(other._state[name].value, GradientFill):
                    # tw.color = self.color.tweenLinear(other.color, t)
                    tw._state[name].value = self._state[name].value.tweenLinear(other._state[name].value, t)
                # self is gradient fill but not other
                elif isinstance(self._state[name].value, GradientFill):
                    othercopy_state_name = self._state[name].value.copy()  # Copy of self's GradientFill figure
                    # Iterate over each keynode of the GradientFill's internal Gradient
                    # and set them to be the constant value of other.
                    for key in othercopy_state_name.gradient.data:
                        value = other._state[name].value
                        othercopy_state_name.gradient[key] = value.copy() if "copy" in dir(value) else value
                    # Apply tween method of the GradientFill class.
                    tw._state[name].value = self._state[name].value.tweenLinear(othercopy_state_name, t)
                # other is gradient fill but not self
                elif isinstance(other._state[name].value, GradientFill):
                    selfcopy_state_name = other._state[name].value.copy()  # Copy of other's GradientFill figure
                    # Iterate over each keynode of the GradientFill's internal Gradient
                    # and set them to be the constant value of other.
                    for key in selfcopy_state_name.gradient.data:
                        value = self._state[name].value
                        selfcopy_state_name.gradient[key] = value.copy() if "copy" in dir(value) else value
                    tw._state[name].value = selfcopy_state_name.tweenLinear(other._state[name].value, t)
                # neither are gradients
                else:
                    value = self._state[name].value
                    if isinstance(value, list) or isinstance(value, tuple):
                        tw._state[name].value = type(value)(map(morpho.numTween, self._state[name].value, other._state[name].value, (t,)*len(value)))
                    elif isinstance(value, np.ndarray):
                        tw._state[name].value = morpho.numTween1(self._state[name].value, other._state[name].value, t)
                    elif isinstance(value, QuadGradientFill):
                        tw._state[name].value = self._state[name].value.tweenLinear(other._state[name].value, t)
                    else:
                        tw._state[name].value = morpho.numTween(self._state[name].value, other._state[name].value, t)

            return tw
        return wrapper
    return decorator

### SPECIAL PARTICULAR GRADIENTS ###

rainbow = lambda: Gradient({
    0:   [1,0,0],
    1/6: [1,1,0],
    2/6: [0,1,0],
    3/6: [0,1,1],
    4/6: [0,0,1],
    5/6: [1,0,1],
    1:   [1,0,0]
})

heatmap = lambda: Gradient({
    0: [0,0,1],
    1/4: [0,1,1],
    1/2: [0,1,0],
    3/4: [1,1,0],
    1: [1,0,0]
    })
