#!/usr/bin/env python3

import math
import re
import io
import copy
import random
import struct
import colorsys

# Class definition for Gradients
# These use the format defined by the GIMP

# The file format is:
#  GIMP Gradient ; literal identifier
#  Name: <utf8-name> ; optional, else get from filename
#  3 ; number of points N
#  ; N  lines like this
#  0.000000 0.166667 0.333333 0.000000 0.000000 1.000000 1.000000 0.000000 0.000000 1.000000 1.000000 0 0
#  The format is
#  start middle end [range 0...1]
#  R G B A left endpoint
#  R G B A right endpoint
#  segment_type coloring_type

# segment-type is
#   GIMP_GRADIENT_SEGMENT_LINEAR,
#   GIMP_GRADIENT_SEGMENT_CURVED,
#   GIMP_GRADIENT_SEGMENT_SINE,
#   GIMP_GRADIENT_SEGMENT_SPHERE_INCREASING,
#   GIMP_GRADIENT_SEGMENT_SPHERE_DECREASING

# color type is
#   GIMP_GRADIENT_SEGMENT_RGB,      /* normal RGB           */
#   GIMP_GRADIENT_SEGMENT_HSV_CCW,  /* counterclockwise hue */
#   GIMP_GRADIENT_SEGMENT_HSV_CW    /* clockwise hue        */


#gradientfile_re = re.compile(r'\s*(RGB|HSV)\s+(Linear|Sinusoidal|CurvedI|CurvedD)\s+(\d+\.?\d+)\s+(\d+)\s+(\d+)\s+(\d+\.?\d+)\s+(\d+)\s+(\d+)')

rgb_re = re.compile(r'\s*(\d+)\s+(\d+)\s+(\d+)')


class FileType:
    MAP, GGR, CS, UGR, ASE = list(range(5))

    @staticmethod
    def guess(s):
        s = s.lower()
        if s.endswith(".map"):
            return FileType.MAP
        elif s.endswith(".cs"):
            return FileType.CS
        elif s.endswith(".ugr"):
            return FileType.UGR
        elif s.endswith(".ase"):
            return FileType.ASE
        else:
            # assume a GIMP gradient, those sometimes don't have extensions
            return FileType.GGR


class Blend:
    LINEAR, CURVED, SINE, SPHERE_INCREASING, SPHERE_DECREASING = list(range(5))


class ColorMode:
    RGB, HSV_CCW, HSV_CW = list(range(3))


class Error(Exception):
    def __init__(self, msg):
        Exception.__init__(self, msg)


class HsvError(Exception):
    def __init__(self, msg):
        Exception.__init__(self, msg)


class Segment:
    EPSILON = 1.0E-7

    def __init__(self, left, left_color, right, right_color, mid=None,
                 blend_mode=Blend.LINEAR,
                 color_mode=ColorMode.RGB):
        self.cmode = color_mode
        self.bmode = blend_mode
        self.left = left
        self.left_color = left_color
        self.right = right
        self.right_color = right_color
        if mid is None:
            self.center()
        else:
            self.mid = mid

    def __copy__(self):
        return Segment(
            self.left, self.left_color[:],
            self.right, self.right_color[:], self.mid,
            self.bmode, self.cmode)

    def __eq__(self, other):
        if other is None:
            return False
        if not isinstance(other, Segment):
            return False
        return self.cmode == other.cmode and \
            self.bmode == other.bmode and \
            self.close(self.left, other.left) and \
            self.close(self.right, other.right) and \
            self.close(self.mid, other.mid) and \
            self.close(self.left_color, other.left_color) and \
            self.close(self.right_color, other.right_color)

    def __ne__(self, other):
        return not self.__eq__(other)

    def left_of(self, other):
        # true if other.left == this.right
        return other.left == self.right and \
            other.left_color[0] == self.right_color[0] and \
            other.left_color[1] == self.right_color[1] and \
            other.left_color[2] == self.right_color[2] and \
            other.left_color[3] == self.right_color[3]

    def right_of(self, other):
        # true if other.right == this.left
        return other.right == self.left and \
            other.right_color[0] == self.left_color[0] and \
            other.right_color[1] == self.left_color[1] and \
            other.right_color[2] == self.left_color[2] and \
            other.right_color[3] == self.left_color[3]

    def close(self, a, b):
        # True if a is nearly == b
        if isinstance(a, list):
            for (ax, bx) in zip(a, b):
                if abs(ax - bx) > 1.0E-5:
                    return False
            return True
        else:
            return abs(a - b) < 1.0E-5

    def center(self):
        self.mid = (self.left + self.right) / 2.0

    def get_linear_factor(self, pos, middle):
        if pos <= middle:
            if middle < Segment.EPSILON:
                return 0.0
            else:
                return 0.5 * pos / middle
        else:
            pos -= middle
            middle = 1.0 - middle
            if middle < Segment.EPSILON:
                return 1.0
            else:
                return 0.5 + 0.5 * pos / middle

    def get_curved_factor(self, pos, middle):
        if middle < Segment.EPSILON:
            middle = Segment.EPSILON

        try:
            return math.pow(pos, (math.log(0.5) / math.log(middle)))
        except ZeroDivisionError:
            # 0^negative number is NaN
            return 0.0

    def get_sine_factor(self, pos, middle):
        pos = self.get_linear_factor(pos, middle)
        return (math.sin((-math.pi / 2.0) + math.pi * pos) + 1.0) / 2.0

    def get_sphere_increasing_factor(self, pos, middle):
        pos = self.get_linear_factor(pos, middle) - 1.0
        return math.sqrt(1.0 - pos * pos)

    def get_sphere_decreasing_factor(self, pos, middle):
        pos = self.get_linear_factor(pos, middle)
        return 1.0 - math.sqrt(1.0 - pos * pos)

    def get_color_at(self, pos):
        'compute the color value for a point in this segment'

        lcol = self.left_color
        rcol = self.right_color
        if self.cmode == ColorMode.HSV_CCW or self.cmode == ColorMode.HSV_CW:
            lcol = [v for v in colorsys.rgb_to_hsv(
                lcol[0], lcol[1], lcol[2])] + [lcol[3]]
            rcol = [v for v in colorsys.rgb_to_hsv(
                rcol[0], rcol[1], rcol[2])] + [rcol[3]]
        if self.cmode == ColorMode.HSV_CCW:
            if lcol[0] >= rcol[0]:
                rcol[0] += 1.0
        if self.cmode == ColorMode.HSV_CW:
            if lcol[0] <= rcol[0]:
                lcol[0] += 1.0

        len = self.right - self.left
        if len < Segment.EPSILON:
            # avoid division by zero
            mpos = 0.5
            pos = 0.5
        else:
            mpos = (self.mid - self.left) / len
            pos = (pos - self.left) / len

        if self.bmode == Blend.LINEAR:
            factor = self.get_linear_factor(pos, mpos)
        elif self.bmode == Blend.CURVED:
            factor = self.get_curved_factor(pos, mpos)
        elif self.bmode == Blend.SINE:
            factor = self.get_sine_factor(pos, mpos)
        elif self.bmode == Blend.SPHERE_INCREASING:
            factor = self.get_sphere_increasing_factor(pos, mpos)
        elif self.bmode == Blend.SPHERE_DECREASING:
            factor = self.get_sphere_decreasing_factor(pos, mpos)

        # Assume RGB mode, for the moment
        RH = lcol[0] + (rcol[0] - lcol[0]) * factor
        GS = lcol[1] + (rcol[1] - lcol[1]) * factor
        BV = lcol[2] + (rcol[2] - lcol[2]) * factor
        A = lcol[3] + (rcol[3] - lcol[3]) * factor

        if self.cmode == ColorMode.RGB:
            return [RH, GS, BV, A]
        if self.cmode == ColorMode.HSV_CCW or self.cmode == ColorMode.HSV_CW:
            if RH > 1:
                RH -= 1
            return [v for v in colorsys.hsv_to_rgb(RH, GS, BV)] + [A]

    def save(self, f, skip_left=False):
        if skip_left:
            # this segment's left end == previous right, so leave it out
            print(f"+{self.mid:6f} {self.right:6f}", end=' ', file=f)
            for x in self.right_color:
                print("%6f" % x, end=' ', file=f)
        else:
            print(
                "%6f %6f %6f" %
                (self.left,
                 self.mid,
                 self.right),
                end=' ',
                file=f)
            for x in self.left_color + self.right_color:
                print("%6f" % x, end=' ', file=f)

        print("%d %d" % (self.bmode, self.cmode), file=f)


class Gradient:
    def __init__(self):
        self.segments = [
            Segment(0, [0, 0, 0, 1.0], 1.0, [1.0, 1.0, 1.0, 1.0])]

        self.name = None
        self.alternate = 0
        self.offset = 0
        self.cobject = None

    def __copy__(self):
        c = Gradient()
        c.name = self.name
        c.alternate = self.alternate
        c.offset = self.offset
        c.segments = copy.deepcopy(self.segments)
        return c

    def __eq__(self, other):
        if other is None:
            return False
        if not isinstance(other, Gradient):
            return False
        if self.name != other.name:
            return False
        if self.segments != other.segments:
            return False
        return True

    def __ne__(self, other):
        return not self.__eq__(other)

    def serialize(self):
        s = io.StringIO()
        self.save(s, True)
        return s.getvalue()

    def save(self, f, compress=False):
        print("GIMP Gradient", file=f)
        if self.name:
            print("Name:", self.name, file=f)
        print(len(self.segments), file=f)
        last = None
        for seg in self.segments:
            compress_seg = compress and last is not None and seg.right_of(last)
            seg.save(f, compress_seg)
            last = seg

    def load_cs(self, f):
        "Load a ColorSchemer (.cs) palette file"
        # this appears to work but file format was reverse engineered
        # so there may be cases unaccounted for
        (ncolors,) = struct.unpack("2xB5x", f.read(8))
        list = []
        for i in range(ncolors):
            (r, g, b, skip) = struct.unpack("<BBBxI", f.read(8))
            entry = (i / float(ncolors), r, g, b, 255)
            f.read(skip)
            (r2, g2, b2, skip) = struct.unpack("BBBB", f.read(4))
            f.read(skip + 1)
            list.append(entry)

        self.load_list(list)

    def load_ase(self, f):
        "Load an Adobe Swatch Exchange (.ase) palette file"
        # File format explained here: https://www.cyotek.com/blog/reading-adobe-swatch-exchange-ase-files-using-csharp

        sig = struct.unpack("BBBB", f.read(4))
        # sig should be ASEF
        if sig[0] != 0x41 or sig[1] != 0x53 or sig[2] != 0x45 or sig[3] != 0x46:
            raise RuntimeError("Not a valid ASE file")

        # version - we only understand 1.0
        (major, minor) = struct.unpack(">HH", f.read(4))
        #print("%d.%d" % (major, minor))
        if major != 1 or minor != 0:
            raise RuntimeError("Unknown ASE version %d.%d" % (major, minor))

        (nblocks,) = struct.unpack(">L", f.read(4))
        #print("nblocks: %d" % nblocks)

        list = []
        for i in range(nblocks):
            (block_type, block_length, name_length) = struct.unpack(">HLH", f.read(8))
            name_format = "%ds" % (name_length * 2)
            #print("%d %d %d" % (block_type, block_length, name_length))
            # print(name_format)
            (name_bytes,) = struct.unpack(name_format, f.read(name_length * 2))
            # print(name_bytes)
            name = name_bytes.decode('utf_16_be')
            #print("name: %s" % name)
            if block_type == 1:
                # color block
                (color_model,) = struct.unpack("4s", f.read(4))
                color_model_name = color_model.decode('ascii')
                #print("color model name: %s" % color_model_name)

                if color_model_name == "RGB ":
                    (r, g, b) = struct.unpack(">3f", f.read(3 * 4))
                    (ri, gi, bi) = (int(r * 255), int(g * 255), int(b * 255))
                    #print("color %f %f %f" % (r*255,g,b))
                    #print("color %x%x%x" % (ri,gi,bi))
                else:
                    raise RuntimeError("Only RGB gradients supported, sorry")

                # global, spot or normal - ignored
                color_type = struct.unpack(">H", f.read(2))

                #print("color type: %d" % color_type)

                entry = (i / float(nblocks), ri, gi, bi, 255)
                list.append(entry)

        self.load_list(list)

    def load_ugr(self, f):
        "Load an ir tree parsed by the translator"
        prev_index = 0.0
        index = 0.0
        segments = []
        prev_color = [0.0, 0.0, 0.0, 0.0]
        for s in f.sections["gradient"].children:
            (name, val) = (s.children[0].name, s.children[1].value)
            if name == "index":
                index = float(val) / 400.0
            elif name == "color":
                icolor = val
                color = [
                    float(icolor & 0xFF) / 256.0,
                    float((icolor >> 8) & 0xFF) / 256.0,
                    float((icolor >> 16) & 0xFF) / 256.0,
                    1.0]
                seg = Segment(
                    prev_index, prev_color,
                    index, color,
                    (prev_index + index) / 2.0,
                    Blend.LINEAR, ColorMode.RGB)
                segments.append(seg)
                prev_index = index
                prev_color = color
            elif name == "smooth":
                pass  # self.smooth = val
            elif name == "title":
                self.name = val

        # append a last chunk from the final value to 1.0
        seg = Segment(
            prev_index, prev_color,
            1.0, prev_color,
            (prev_index + 1.0) / 2.0,
            Blend.LINEAR, ColorMode.RGB)
        segments.append(seg)

        self.segments = segments

    def load_gimp_gradient(self, f):
        new_segments = []
        name = None

        right = rr = rg = rb = ra = 0  # should never be used

        line = f.readline()
        if line.startswith("Name:"):
            name = line[5:].strip()
            line = f.readline()
        num_vals = int(line)
        for i in range(num_vals):
            line = f.readline()
            if line[:1] == "+":
                # a compressed continuation, use last vals
                left = right
                lr, lg, lb, la = rr, rg, rb, ra
                [mid, right, rr, rg, rb, ra, bmode, cmode] = line.split()
            else:
                list_elements = line.split()
                [left, mid, right,
                 lr, lg, lb, la,
                 rr, rg, rb, ra,
                 bmode, cmode] = list_elements[0:13]

            seg = Segment(
                float(left), [float(lr), float(lg), float(lb), float(la)],
                float(right), [float(rr), float(rg), float(rb), float(ra)],
                float(mid),
                int(bmode), int(cmode))
            new_segments.append(seg)

        self.segments = new_segments
        self.name = name

    def load(self, f):
        if hasattr(f, "readline"):
            # assume this is a file type
            line = f.readline()
            if line == "GIMP Gradient\n":
                return self.load_gimp_gradient(f)
            elif line[:2] == b'\x03\x00':
                # a .cs file, we suspect
                f.seek(0)
                return self.load_cs(f)
            elif line[:4] == b'ASEF':
                # probably an ASE file
                f.seek(0)
                return self.load_ase(f)
            else:
                f.seek(0)
                return self.load_map_file(f)
        else:
            # assume it's a translated UGR file
            return self.load_ugr(f)

    def compare_colors(self, c1, c2, maxdiff=0):
        # return true if floating-point colors c1 and c2 are close
        # enough that they would be equal when truncated to 8 bits
        for (a, b) in zip(c1, c2):
            a8 = int(a * 255.0)
            b8 = int(b * 255.0)
            if abs(a8 - b8) > maxdiff:
                return False
        return True

    def load_map_file(self, mapfile, maxdiff=0):
        i = 0
        colorlist = []
        solid = (0, 0, 0, 255)
        for line in mapfile:
            m = rgb_re.match(line)
            if m is not None:
                (r, g, b) = (min(255, int(m.group(1))),
                             min(255, int(m.group(2))),
                             min(255, int(m.group(3))))

                if i == 0:
                    # first color is inside solid color
                    solid = (r, g, b, 255)
                else:
                    colorlist.append(((i - 1) / 255.0, r, g, b, 255))
            i += 1
        self.load_list(colorlist, maxdiff)
        return solid

    def is_coolorable(self):
        'Indicate if we can create a coolors url for this gradient'
        # Coolors.co can only do up to 10 colors per palette
        return len(self.segments) < 11

    def get_coolor_url(self):
        'Generate a URL which encodes a palette. This can be opened to edit on the website coolors.co'
        clist = []
        for s in self.segments:
            color = s.left_color
            (r, g, b, a) = (int(x * 255.0) for x in color)
            colorstring = f"{r:02x}{g:02x}{b:02x}"
            clist.append(colorstring)
        return "https://coolors.co/" + ("-".join(clist))

    def load_from_url(self, url):
        'Parse a URL which encodes a palette. We don\'t actually fetch it, the data is in the URL itself'
        try:
            if not url.startswith("https://coolors.co/"):
                return False
            url = url[19:]  # remove domain
            parts = url.split('-')

            colorlist = []
            ncolors = len(parts)
            i = 0
            for p in parts:
                (r, g, b) = (int(p[0:2], 16), int(p[2:4], 16), int(p[4:6], 16))
                entry = (i / ncolors, r, g, b, 255)
                # print(entry)
                colorlist.append(entry)
                i += 1
            self.load_list(colorlist, maxdiff=-1)
            return True
        except RuntimeError as err:
            print(err)
            return False

    def load_list(self, l, maxdiff=0):
        # a colorlist is a simplified gradient, of the form
        # (index, r, g, b, a) (colors are 0-255 ints)
        # each index is the left-hand end of the segment

        # each colorlist entry is mapped to a segment endpoint

        if len(l) == 0:
            raise Error("No colors found")

        new_segments = []
        last_index = 0.0
        last_color = [0.0, 0.0, 0.0, 1.0]
        before_last_color = [-1000.0, -1000.0, -
                             1000.0, -1000.0]  # meaningless color
        before_last_index = -1.0

        for (index, r, g, b, a) in l:
            color = [r / 255.0, g / 255.0, b / 255.0, a / 255.0]
            if index != last_index:
                test_segment = Segment(
                    before_last_index,
                    before_last_color,
                    index,
                    color)
                if self.compare_colors(
                        test_segment.get_color_at(last_index), last_color, maxdiff):
                    # can compress, update in place
                    new_segments[-1].right_color = color
                    new_segments[-1].right = index
                    new_segments[-1].center()
                else:
                    new_segments.append(
                        Segment(last_index, last_color, index, color))

                    before_last_index = last_index
                    before_last_color = last_color
            last_color = color
            last_index = index

        # fix gradient by adding extra flat section if last index not 1.0
        if new_segments[-1].right != 1.0:
            new_segments.append(
                Segment(new_segments[-1].right, last_color, 1.0, last_color))

        self.segments = new_segments

    def set_color(self, seg_id, is_left, r, g, b):
        if seg_id < 0 or seg_id >= len(self.segments):
            return False
        seg = self.segments[seg_id]
        if is_left:
            seg.left_color = [r, g, b, seg.left_color[3]]
        else:
            seg.right_color = [r, g, b, seg.right_color[3]]

        return True

    def complementaries(self, base_color):
        # return some other colors that "go" with this one
        hsv = RGBtoHSV(base_color)

        (h, s, v, a) = hsv

        # take 2 colors which are almost triads
        h = hsv[0]
        delta = random.gauss(0.0, 0.8)
        h2 = math.fmod(h + 2.5 + delta, 6.0)
        h3 = math.fmod(h + 3.5 - delta, 6.0)

        # take darker and lighter versions
        v = hsv[2]
        vlight = self.clamp(v * 1.5, 0.0, 1.0)
        vdark = v * 0.5

        colors = [
            [h, s, vdark, a],
            [h, s, v, a],
            [h, s, vlight, a],
            [h2, s, vlight, a],
            [h2, s, v, a],
            [h2, s, vdark, a],
            [h3, s, vdark, a],
            [h3, s, v, a],
            [h3, s, vlight, a]]

        colors = [HSVtoRGB(x) for x in colors]
        return colors

    def randomize(self, length):
        if random.random() < 0.5:
            self.randomize_complementary(length)
        else:
            self.randomize_spheres((int(random.random() * 4) + 3) * 2)

    def randomize_complementary(self, length):
        base = [random.random(), random.random(), random.random(), 1.0]
        colors = self.complementaries(base)
        self.segments = []
        prev_index = 0.0
        prev_color = colors[0]
        first_color = prev_color
        for i in range(9 - 1):
            index = float(i + 1) / length
            color = colors[i]
            self.segments.append(
                Segment(prev_index, prev_color, index, color))
            prev_color = color
            prev_index = index

        self.segments.append(
            Segment(prev_index, prev_color, 1.0, first_color))  # make it wrap

    def random_bright_color(self):
        return HSVtoRGB(
            [random.random() * 360.0,
             random.random(),
             random.random() * 0.6 + 0.4,
             1.0])

    def randomize_spheres(self, length):
        self.segments = []
        prev_index = 0.0
        prev_color = self.random_bright_color()
        first_color = prev_color
        for i in range(length - 1):
            index = float(i + 1) / length
            if i % 2 == 1:
                color = self.random_bright_color()
                blend = Blend.SPHERE_INCREASING
            else:
                color = [0.0, 0.0, 0.0, 1.0]
                blend = Blend.SPHERE_DECREASING
            self.segments.append(
                Segment(prev_index, prev_color, index, color, None, blend))
            prev_color = color
            prev_index = index

        self.segments.append(
            Segment(prev_index, prev_color, 1.0, first_color))  # make it wrap

    def get_color_at(self, pos):
        # returns the color at position x (0 <= x <= 1.0)
        seg = self.get_segment_at(pos)
        return seg.get_color_at(pos)

    def get_segment_at(self, pos):
        # returns the segment in which pos resides.
        if pos < 0.0:
            raise IndexError("Must be between 0 and 1, is %s" % pos)
        for seg in self.segments:
            if pos <= seg.right:
                return seg

        # not found - must be > 1.0
        raise IndexError("Must be between 0 and 1, is %s" % pos)

    def get_index_at(self, pos):
        # returns the index of the segment in which pos resides
        if pos < 0.0:
            raise IndexError("Must be between 0 and 1")
        length = len(self.segments)
        for i in range(length):
            if pos <= self.segments[i].right:
                return i

        # not found - must be > 1.0
        raise IndexError("Must be between 0 and 1")

    def add(self, segindex):
        # split the segment which contains point x in half
        seg = self.segments[segindex]

        if segindex + 1 < len(self.segments):
            # copy info from next segment to right
            segright = self.segments[segindex + 1]
            right_index = segright.left
            right_color = segright.left_color
        else:
            # adding at right-hand end
            right_index = 1.0
            right_color = seg.right_color

        s_len = (seg.right - seg.left)
        s_mid = seg.left + s_len * 0.5
        newcol = self.get_color_at(s_mid)

        # update existing segment to occupy left half
        seg.right = s_mid
        seg.right_color = newcol
        seg.center()

        # add new segment to fill right half
        self.segments.insert(
            segindex + 1,
            Segment(s_mid, newcol,
                    right_index, right_color,
                    None,
                    seg.bmode, seg.cmode))

    def remove(self, segindex, smooth=False):
        # remove the segment which contains point x
        # extend each of our neighbors so they get half our space each
        if len(self.segments) < 2:
            raise Error("Can't remove last segment")

        seg = self.segments[segindex]

        if segindex > 0:
            # we have a previous segment
            if segindex + 1 < len(self.segments):
                # and we have a next. Move them both to touch in the middle
                self.segments[segindex - 1].right = seg.mid
                self.segments[segindex + 1].left = seg.mid
                self.segments[segindex - 1].center()
                self.segments[segindex + 1].center()
                if smooth:
                    midcolor = seg.get_color_at(seg.mid)
                    self.segments[segindex -
                                  1].right_color = copy.copy(midcolor)
                    self.segments[segindex +
                                  1].left_color = copy.copy(midcolor)
            else:
                # just a left-hand neighbor, let that take over
                self.segments[segindex - 1].right = 1.0
                if smooth:
                    self.segments[segindex - 1].right_color = \
                        copy.copy(self.segments[segindex].right_color)

                self.segments[segindex - 1].center()
        else:
            # we must have a later segment
            self.segments[segindex + 1].left = 0.0
            if smooth:
                self.segments[segindex + 1].left_color = \
                    copy.copy(self.segments[segindex].left_color)
            self.segments[segindex + 1].center()

        self.segments.pop(segindex)

    def clamp(self, a, min, max):
        if a > max:
            return max
        elif a < min:
            return min
        else:
            return a

    def set_left(self, i, pos):
        # set left end of segment i to pos, if possible
        if i < 0 or i >= len(self.segments):
            raise IndexError("No such segment")

        if i == 0:
            # can't move left-hand end of entire gradient
            return 0.0
        else:
            pos = self.clamp(pos,
                             self.segments[i - 1].mid + Segment.EPSILON,
                             self.segments[i].mid - Segment.EPSILON)
            self.segments[i - 1].right = self.segments[i].left = pos
            return pos

    def set_right(self, i, pos):
        # set left end of segment i to pos, if possible
        if i < 0 or i >= len(self.segments):
            raise IndexError("No such segment")

        max = len(self.segments) - 1
        if i == max:
            # can't move right-hand end of entire gradient
            return 1.0
        else:
            pos = self.clamp(pos,
                             self.segments[i].mid + Segment.EPSILON,
                             self.segments[i + 1].mid - Segment.EPSILON)

            self.segments[i + 1].left = self.segments[i].right = pos
            return pos

    def set_middle(self, i, pos):
        # set middle of segment i to pos, if possible
        if i < 0 or i >= len(self.segments):
            raise IndexError("No such segment")

        pos = self.clamp(pos,
                         self.segments[i].left + Segment.EPSILON,
                         self.segments[i].right - Segment.EPSILON)

        self.segments[i].mid = pos
        return pos

# These two are adapted from the algorithms at
# http://www.cs.rit.edu/~ncs/color/t_convert.html


def RGBtoHSV(rgb):
    hsv = [0, 0, 0, rgb[3]]
    trgb = rgb[0:3]
    trgb.sort()

    min = trgb[0]
    max = trgb[2]

    delta = float(max - min)
    hsv[2] = max

    if delta == 0:
        # r = g = b = 0        # s = 0, v is undefined
        hsv[1] = 0
        hsv[0] = -1
    else:
        hsv[1] = delta / max

        if rgb[0] == max:
            # between yellow & magenta
            hsv[0] = (rgb[1] - rgb[2]) / delta
        elif rgb[1] == max:
            hsv[0] = 2 + (rgb[2] - rgb[0]) / delta    # between cyan & yellow
        else:
            hsv[0] = 4 + (rgb[0] - rgb[1]) / delta    # between magenta & cyan

    hsv[0] *= 60          # degrees
    if hsv[0] < 0:
        hsv[0] += 360

    return hsv


def HSVtoRGB(hsv):
    rgb = [0, 0, 0, hsv[3]]  # pass through alpha channel

    hsv[0] /= 60

    if hsv[1] == 0:
        return [hsv[2], hsv[2], hsv[2]]

    i = int(hsv[0])
    f = hsv[0] - i        # decimal bit of hue
    p = hsv[2] * (1 - hsv[1])
    q = hsv[2] * (1 - hsv[1] * f)
    t = hsv[2] * (1 - hsv[1] * (1 - f))

    if i == 0:
        rgb[0] = hsv[2]
        rgb[1] = t
        rgb[2] = p
    elif i == 1:
        rgb[0] = q
        rgb[1] = hsv[2]
        rgb[2] = p
    elif i == 2:
        rgb[0] = p
        rgb[1] = hsv[2]
        rgb[2] = t
    elif i == 3:
        rgb[0] = p
        rgb[1] = q
        rgb[2] = hsv[2]
    elif i == 4:
        rgb[0] = t
        rgb[1] = p
        rgb[2] = hsv[2]
    elif i == 5:
        rgb[0] = hsv[2]
        rgb[1] = p
        rgb[2] = q

    return rgb
