from PIL import Image
from .. import objects
from .. import NVector, Color
from ..utils.color import from_uint8


class Polygen:
    def __init__(self, x, y):
        self.vertices = [
            NVector(x, y),
            NVector(x+1, y),
            NVector(x+1, y+1),
            NVector(x, y+1),
        ]
        self._has_x = False
        self._has_y = False

    def add_pixel_x(self, x, y):
        i = self.vertices.index(NVector(x, y))
        if len(self.vertices) > i and self.vertices[i+1] == NVector(x, y+1):
            self._has_x = True
            self.vertices.insert(i+1, NVector(x+1, y))
            self.vertices.insert(i+2, NVector(x+1, y+1))
        else:
            raise ValueError()

    def add_pixel_x_neg(self, x, y):
        i = self.vertices.index(NVector(x+1, y))
        if i > 0 and self.vertices[i-1] == NVector(x+1, y+1):
            self._has_x = True
            self.vertices.insert(i, NVector(x, y))
            self.vertices.insert(i, NVector(x, y+1))
        else:
            raise ValueError()

    def add_pixel_y(self, x, y):
        i = self.vertices.index(NVector(x, y))
        if i > 0 and self.vertices[i-1] == NVector(x+1, y):
            self._has_y = True
            if i > 1 and self.vertices[i-2] == NVector(x+1, y+1):
                self.vertices[i-1] = NVector(x, y+1)
            else:
                self.vertices.insert(i, NVector(x, y+1))
                self.vertices.insert(i, NVector(x+1, y+1))
        else:
            raise ValueError()

    def _to_rect(self, id1, id2, scale):
        p1 = self.vertices[id1] * scale
        p2 = self.vertices[id2] * scale
        return objects.Rect((p1+p2)/2, p2-p1)

    def to_shape(self, scale):
        if not self._has_x or not self._has_y:
            return self._to_rect(0, int(len(self.vertices)/2), scale)
        bez = objects.Bezier()
        bez.closed = True
        for point in self.vertices:
            point = point * scale
            if len(bez.vertices) > 1 and (
                bez.vertices[-1].x == bez.vertices[-2].x == point.x or
                bez.vertices[-1].y == bez.vertices[-2].y == point.y
            ):
                bez.vertices[-1] = point
            else:
                bez.add_point(point)

        if len(bez.vertices) > 2 and bez.vertices[0].x == bez.vertices[-1].x == bez.vertices[-2].x:
            bez.vertices.pop()
            bez.out_tangents.pop()
            bez.in_tangents.pop()
        return objects.Path(bez)


def pixel_to_layer_paths(raster, scale=1, stroke_width=None):
    layer = objects.ShapeLayer()
    groups = {}
    processed = set()
    xneg_candidates = set()
    if stroke_width is None:
        stroke_width = 0.1 * scale

    def avail(x, y):
        rid = (x, y)
        return not (
            x < 0 or x >= raster.width or y >= raster.height or
            rid in processed or raster.getpixel(rid) != colort
        )

    def recurse(gen, x, y, xneg):
        processed.add((x, y))
        if avail(x+1, y):
            gen.add_pixel_x(x+1, y)
            recurse(gen, x+1, y, False)
        if avail(x, y+1):
            gen.add_pixel_y(x, y+1)
            recurse(gen, x, y+1, True)
        if xneg and avail(x-1, y):
            xneg_candidates.add((x-1, y))

    for y in range(raster.height):
        for x in range(raster.width):
            pid = (x, y)
            colort = raster.getpixel(pid)
            if colort[-1] == 0 or pid in processed:
                continue

            gen = Polygen(x, y)
            xneg_candidates = set()
            recurse(gen, x, y, False)
            xneg_candidates -= processed
            while xneg_candidates:
                p = next(iter(sorted(xneg_candidates, key=lambda t: (t[1], t[0]))))
                gen.add_pixel_x_neg(*p)
                recurse(gen, p[0], p[1], True)
                processed.add(p)
                xneg_candidates -= processed

            g = groups.setdefault(colort, set())
            g.add(gen.to_shape(scale))

    # Debug
    #for colort, rects in groups.items():
        #for rect in rects:
            #g = layer.add_shape(objects.Group())
            #g.shapes = [rect] + g.shapes
            #g.name = "".join("%02x" % c for c in colort)
            #color = from_uint8(*colort[:3])
            #opacity = colort[-1] / 255 * 100
            #stroke = g.add_shape(objects.Stroke(NVector(0, 0, 0), stroke_width))
            #fill = g.add_shape(objects.Fill())
            #fill.color.value = color
            #fill.opacity.value = 20

    for colort, rects in groups.items():
        g = layer.add_shape(objects.Group())
        g.shapes = list(rects) + g.shapes
        g.name = "".join("%02x" % c for c in colort)
        color = from_uint8(*colort[:3])
        opacity = colort[-1] / 255 * 100
        fill = g.add_shape(objects.Fill())
        fill.color.value = color
        fill.opacity.value = opacity
        if stroke_width > 0:
            stroke = g.add_shape(objects.Stroke(color, stroke_width))
            stroke.opacity.value = opacity

    return layer


def pixel_add_layer_paths(animation, raster):
    return animation.add_layer(pixel_to_layer_paths(raster))


def pixel_add_layer_rects(animation, raster):
    layer = animation.add_layer(objects.ShapeLayer())
    last_rects = {}
    groups = {}

    def merge_up():
        if last_rect and last_rect._start in last_rects:
            yrect = last_rects[last_rect._start]
            if yrect.size.value.x == last_rect.size.value.x and yrect._color == last_rect._color:
                groups[last_rect._color].remove(last_rect)
                yrect.position.value.y += 0.5
                yrect.size.value.y += 1
                rects[last_rect._start] = yrect

    def group(colort):
        return groups.setdefault(colort, set())

    for y in range(raster.height):
        rects = {}
        last_color = None
        last_rect = None
        for x in range(raster.width):
            colort = raster.getpixel((x, y))
            if colort[-1] == 0:
                last_color = 0
                continue
            yrect = last_rects.get(x, None)
            if colort == last_color:
                last_rect.position.value.x += 0.5
                last_rect.size.value.x += 1
            elif yrect and colort == yrect._color and yrect.size.value.x == 1:
                yrect.position.value.y += 0.5
                yrect.size.value.y += 1
                rects[x] = yrect
                last_color = last_rect = colort = None
            else:
                merge_up()
                g = group(colort)
                last_rect = objects.Rect()
                g.add(last_rect)
                last_rect.size.value = NVector(1, 1)
                last_rect.position.value = NVector(x + 0.5, y + 0.5)
                rects[x] = last_rect
                last_rect._start = x
                last_rect._color = colort
            last_color = colort
        merge_up()
        last_rects = rects

    for colort, rects in groups.items():
        g = layer.add_shape(objects.Group())
        g.shapes = list(rects) + g.shapes
        g.name = "".join("%02x" % c for c in colort)
        fill = g.add_shape(objects.Fill())
        fill.color.value = from_uint8(*colort[:3])
        fill.opacity.value = colort[-1] / 255 * 100
        stroke = g.add_shape(objects.Stroke(fill.color.value, 0.1))
        stroke.opacity.value = fill.opacity.value
    return layer


def _vectorizing_func(filenames, frame_delay, framerate, callback):
    if not isinstance(filenames, list):
        filenames = [filenames]

    animation = objects.Animation(0, framerate)
    nframes = 0
    time = 0

    for filename in filenames:
        if isinstance(filename, Image.Image):
            raster = filename
        else:
            raster = Image.open(filename)

        if nframes == 0:
            animation.width = raster.width
            animation.height = raster.height

        if not hasattr(raster, "is_animated"):
            raster.n_frames = 1
            raster.seek = lambda x: None

        for frame in range(raster.n_frames):
            raster.seek(frame)
            new_im = Image.new("RGBA", raster.size)
            new_im.paste(raster)
            duration = frame_delay
            image_duration = raster.info.get("duration", 0)
            if image_duration:
                duration = framerate * image_duration / 1000
            callback(animation, new_im, nframes + frame, time, duration)
            time += duration
            new_im.close()
        nframes += raster.n_frames

    animation.out_point = time
    return animation


def raster_to_embedded_assets(filenames, frame_delay=1, framerate=60, embed_format=None):
    """!
    @brief Loads external assets
    """
    def callback(animation, raster, frame, time, duration):
        asset = objects.assets.Image.embedded(raster, embed_format)
        animation.assets.append(asset)
        layer = animation.add_layer(objects.ImageLayer(asset.id))
        layer.in_point = time
        layer.out_point = layer.in_point + duration

    return _vectorizing_func(filenames, frame_delay, framerate, callback)


def raster_to_linked_assets(filenames, frame_delay=1, framerate=60):
    """!
    @brief Loads external assets
    """
    animation = objects.Animation(frame_delay * len(filenames), framerate)

    for frame, filename in enumerate(filenames):
        asset = objects.assets.Image.linked(filename)
        animation.assets.append(asset)
        layer = animation.add_layer(objects.ImageLayer(asset.id))
        layer.in_point = frame * frame_delay
        layer.out_point = layer.in_point + frame_delay

    return animation


def pixel_to_animation(filenames, frame_delay=1, framerate=60):
    """!
    @brief Converts pixel art to vector
    """
    def callback(animation, raster, frame, time, duration):
        layer = pixel_add_layer_rects(animation, raster.convert("RGBA"))
        layer.in_point = time
        layer.out_point = layer.in_point + duration

    return _vectorizing_func(filenames, frame_delay, framerate, callback)


def pixel_to_animation_paths(filenames, frame_delay=1, framerate=60):
    """!
    @brief Converts pixel art to vector paths

    Slower and yields larger files compared to pixel_to_animation,
    but it produces a single shape for each area with the same color.
    Mostly useful when you want to add your own animations to the loaded image
    """
    def callback(animation, raster, frame, time, duration):
        layer = pixel_add_layer_paths(animation, raster.convert("RGBA"))
        layer.in_point = time
        layer.out_point = layer.in_point + duration

    return _vectorizing_func(filenames, frame_delay, framerate, callback)
