# Copyright (c) 2015 Ultimaker B.V.
# Cura is released under the terms of the AGPLv3 or higher.

from UM.Mesh.MeshData import MeshData
from UM.Mesh.MeshBuilder import MeshBuilder
from UM.Math.Color import Color
from UM.Math.Vector import Vector

import numpy
import math
import copy

class LayerData(MeshData):
    def __init__(self):
        super().__init__()
        self._layers = {}
        self._element_counts = {}

    def addLayer(self, layer):
        if layer not in self._layers:
            self._layers[layer] = Layer(layer)

    def addPolygon(self, layer, type, data, line_width):
        if layer not in self._layers:
            self.addLayer(layer)

        p = Polygon(self, type, data, line_width)
        self._layers[layer].polygons.append(p)

    def getLayer(self, layer):
        if layer in self._layers:
            return self._layers[layer]

    def getLayers(self):
        return self._layers

    def getElementCounts(self):
        return self._element_counts

    def setLayerHeight(self, layer, height):
        if layer not in self._layers:
            self.addLayer(layer)

        self._layers[layer].setHeight(height)

    def setLayerThickness(self, layer, thickness):
        if layer not in self._layers:
            self.addLayer(layer)

        self._layers[layer].setThickness(thickness)

    def build(self):
        vertex_count = 0
        for layer, data in self._layers.items():
            vertex_count += data.vertexCount()

        vertices = numpy.empty((vertex_count, 3), numpy.float32)
        colors = numpy.empty((vertex_count, 4), numpy.float32)
        indices = numpy.empty((vertex_count, 2), numpy.int32)

        offset = 0
        for layer, data in self._layers.items():
            offset = data.build(offset, vertices, colors, indices)
            self._element_counts[layer] = data.elementCount

        self.clear()
        self.addVertices(vertices)
        self.addColors(colors)
        self.addIndices(indices.flatten())

class Layer():
    def __init__(self, id):
        self._id = id
        self._height = 0.0
        self._thickness = 0.0
        self._polygons = []
        self._element_count = 0

    @property
    def height(self):
        return self._height

    @property
    def thickness(self):
        return self._thickness

    @property
    def polygons(self):
        return self._polygons

    @property
    def elementCount(self):
        return self._element_count

    def setHeight(self, height):
        self._height = height

    def setThickness(self, thickness):
        self._thickness = thickness

    def vertexCount(self):
        result = 0
        for polygon in self._polygons:
            result += polygon.vertexCount()

        return result

    def build(self, offset, vertices, colors, indices):
        result = offset
        for polygon in self._polygons:
            if polygon.type == Polygon.InfillType or polygon.type == Polygon.MoveCombingType or polygon.type == Polygon.MoveRetractionType:
                continue

            polygon.build(result, vertices, colors, indices)
            result += polygon.vertexCount()
            self._element_count += polygon.elementCount

        return result

    def createMesh(self):
        return self.createMeshOrJumps(True)
        
    def createJumps(self):
        return self.createMeshOrJumps(False)
        
    def createMeshOrJumps(self, make_mesh):
        builder = MeshBuilder()

        for polygon in self._polygons:
            if make_mesh and (polygon.type == Polygon.MoveCombingType or polygon.type == Polygon.MoveRetractionType):
                continue
            if not make_mesh and not (polygon.type == Polygon.MoveCombingType or polygon.type == Polygon.MoveRetractionType):
                continue

            poly_color = polygon.getColor()

            points = numpy.copy(polygon.data)
            if polygon.type == Polygon.InfillType or polygon.type == Polygon.SkinType or polygon.type == Polygon.SupportInfillType:
                points[:,1] -= 0.01
            if polygon.type == Polygon.MoveCombingType or polygon.type == Polygon.MoveRetractionType:
                points[:,1] += 0.01

            normals = polygon.getNormals()

            # Scale all by the line width of the polygon so we can easily offset.
            normals *= (polygon.lineWidth / 2)

            #TODO: Use numpy magic to perform the vertex creation to speed up things.
            for i in range(len(points)):
                start = points[i - 1]
                end = points[i]

                normal = normals[i - 1]

                point1 = Vector(data = start - normal)
                point2 = Vector(data = start + normal)
                point3 = Vector(data = end + normal)
                point4 = Vector(data = end - normal)

                builder.addQuad(point1, point2, point3, point4, color = poly_color)

        return builder.getData()

class Polygon():
    NoneType = 0
    Inset0Type = 1
    InsetXType = 2
    SkinType = 3
    SupportType = 4
    SkirtType = 5
    InfillType = 6
    SupportInfillType = 7
    MoveCombingType = 8
    MoveRetractionType = 9

    def __init__(self, mesh, type, data, line_width):
        self._mesh = mesh
        self._type = type
        self._data = data
        self._line_width = line_width / 1000

        self._color = self.__color_map[type]

    def build(self, offset, vertices, colors, indices):
        self._begin = offset
        self._end = self._begin + len(self._data) - 1

        vertices[self._begin:self._end + 1, :] = self._data[:, :]
        colors[self._begin:self._end + 1, :] = numpy.array([self._color.r * 0.5, self._color.g * 0.5, self._color.b * 0.5, self._color.a], numpy.float32)

        for i in range(self._begin, self._end):
            indices[i, 0] = i
            indices[i, 1] = i + 1

        indices[self._end, 0] = self._end
        indices[self._end, 1] = self._begin

    def getColor(self):
        return self._color

    def vertexCount(self):
        return len(self._data)

    @property
    def type(self):
        return self._type

    @property
    def data(self):
        return self._data

    @property
    def elementCount(self):
        return ((self._end - self._begin) + 1) * 2 #The range of vertices multiplied by 2 since each vertex is used twice

    @property
    def lineWidth(self):
        return self._line_width

    # Calculate normals for the entire polygon using numpy.
    def getNormals(self):
        normals = numpy.copy(self._data)
        normals[:,1] = 0.0 # We are only interested in 2D normals

        # Calculate the edges between points.
        # The call to numpy.roll shifts the entire array by one so that
        # we end up subtracting each next point from the current, wrapping
        # around. This gives us the edges from the next point to the current
        # point.
        normals[:] = normals[:] - numpy.roll(normals, -1, axis = 0)
        # Calculate the length of each edge using standard Pythagoras
        lengths = numpy.sqrt(normals[:,0] ** 2 + normals[:,2] ** 2)
        # The normal of a 2D vector is equal to its x and y coordinates swapped
        # and then x inverted. This code does that.
        normals[:,[0, 2]] = normals[:,[2, 0]]
        normals[:,0] *= -1

        # Normalize the normals.
        normals[:,0] /= lengths
        normals[:,2] /= lengths

        return normals

    __color_map = {
        NoneType: Color(1.0, 1.0, 1.0, 1.0),
        Inset0Type: Color(1.0, 0.0, 0.0, 1.0),
        InsetXType: Color(0.0, 1.0, 0.0, 1.0),
        SkinType: Color(1.0, 1.0, 0.0, 1.0),
        SupportType: Color(0.0, 1.0, 1.0, 1.0),
        SkirtType: Color(0.0, 1.0, 1.0, 1.0),
        InfillType: Color(1.0, 0.74, 0.0, 1.0),
        SupportInfillType: Color(0.0, 1.0, 1.0, 1.0),
        MoveCombingType: Color(0.0, 0.0, 1.0, 1.0),
        MoveRetractionType: Color(0.5, 0.5, 1.0, 1.0),
    }
