/**
 * Eine Zeilposition mit einer beliebigförmnigen DropZone und mehreren Slot-Positionen in die mehrere Teile einrasten
 *
 * @constructor
 * @param {Box} rDestinationBox Die Box in der die Drop Zone liegen soll
 * @param {Polygon [[x1,x2], ... , [xn, yn]]} rDropZone Array von zweistelligen Arrays die die Punkte in einem Polygon darstellen
 * @param {Punkte [[x1,x2], ... , [xn, yn]]} pDestinationPositions Die Positionen in die die Teile einrasten
 */
function AssignmentDestination(rDestinationBox, rDropZone, pDestinationPositions) {
    this.rDestinationBox = rDestinationBox;
    this.rDropZone = rDropZone;
    this.pDestinationPositions = pDestinationPositions;
    this.pFreeDestinationPositions = [...pDestinationPositions];
    this.pJsonId = newJsonId();

    /**
     * Bestimmt welcher Slot als nächstes benutzt wird
     * @returns Punkt [x,y]
     */
    this.next = function () {
        var pos = this.pFreeDestinationPositions.shift();
        return pos;
    };

    /**
     * Macht einen Slot wieder nutzbar
     * @param {Punkt [x,y]} pPosition - Position
     */
    this.free = function (pPosition) {
        if (pPosition != null) {
            this.pFreeDestinationPositions.push(pPosition);
            var newFree = [];
            for (let i = 0; i < this.pDestinationPositions.length; i++) {
                var x = this.pDestinationPositions[i];
                for (let j = 0; j < this.pFreeDestinationPositions.length; j++) {
                    var y = this.pFreeDestinationPositions[j];

                    if (x[0] == y[0] && x[1] == y[1]) {
                        newFree.push(x);
                    }
                }
            }
            //if (this.pFreeDestinationPositions.length != newFree.length) console.log("ERROR");
            this.pFreeDestinationPositions = newFree;
        }
    };

    /**
     * Entfernt eine Position aus den noch freien slots
     * @param {Punkt [x,y]} pPosition - Position die entfernt werden soll
     */
    this.remove = function (pPosition) {
        this.pFreeDestinationPositions = this.pFreeDestinationPositions.filter(function (v, i, a) {
            return v[0] != pPosition[0] || v[1] != pPosition[1];
        });
    };

    /**
     * setzt die Freien Slots der DestinationPositions auf den Ausgangszustand zurück
     */
    this.reset = function () {
        this.pFreeDestinationPositions = [...pDestinationPositions];
    };
}

/**
 * Klasse AssignmentPiece
 * Teil das in das Assignment eingeordnet werden kann. Kann Text oder Bild sein
 *
 * @constructor
 * @param pStartPosX {float} Startposition.x
 * @param pStartPosY {float} Startposition.y
 * @param rStartBox {Box} Starterbox
 * @param pWidth {float} Breite
 * @param pHeight {float} Höhe
 * @param rDestination {AssignmentDestination} Zielbox
 */
function AssignmentPiece(pStartPosX, pStartPosY, rStartBox, pWidth, pHeight, rDestination) {
    this.pType = "AssignmentPiece";
    this.pStartPosX = pStartPosX;
    this.pStartPosY = pStartPosY;
    this.pPosX = this.pStartPosX;
    this.pPosY = this.pStartPosY;
    this.pInitialPosX = this.pPosX;
    this.pInitialPosY = this.pPosY;
    this.pWidth = pWidth;
    this.pHeight = pHeight;
    this.pInitialWidth = pWidth;
    this.pInitialHeight = pHeight;
    this.rSelector;
    this.pIsMouseDown = false;
    this.pLastPosX = 0;
    this.pLastPosY = 0;
    this.pSolved = false;
    this.pScale = 1;
    this.rBox = rStartBox;
    this.rStartBox = rStartBox;
    this.rPositions = [];
    this.pJsonId = newJsonId();
    //this.targetElement = targetElement;
    this.color_solved = "rgb(0,255,0)";
    this.color_unsolved = "rgb(255,0,0)";
    this.rDestination = rDestination;
    this.pCheckWhenDone = false;
    this.pAllSolved = false;
    this.pDropScale = 0.6;

    /**
     * Setzt die Position des Teils
     * @param {float} pPosX X-Position
     * @param {float} pPosY Y-Position
     */
    this.setPos = function (pPosX, pPosY) {
        // set position of element
        this.pPosX = pPosX;
        this.pPosY = pPosY;
        $(this.rSelector).css("left", pPosX * this.pScale + "px");
        $(this.rSelector).css("top", pPosY * this.pScale + "px");

        // limit position to contentFrame
        var contentFrame = $("#contentFrame");
        var contentFrameOffset = contentFrame.offset();
        var contentFrameWidth = contentFrame.width();
        var contentFrameHeight = contentFrame.height();
        var contentFrameLeft = contentFrameOffset.left;
        var contentFrameTop = contentFrameOffset.top;
        var contentFrameRight = contentFrameLeft + contentFrameWidth;
        var contentFrameBottom = contentFrameTop + contentFrameHeight;

        var thisOffset = $(this.rSelector).offset();
        var thisWidth = $(this.rSelector).width();
        var thisHeight = $(this.rSelector).height();
        var thisLeft = thisOffset.left;
        var thisTop = thisOffset.top;
        var thisRight = thisLeft + thisWidth;
        var thisBottom = thisTop + thisHeight;

        if (thisLeft < contentFrameLeft) {
            pPosX += contentFrameLeft - thisLeft;
        } else if (thisRight > contentFrameRight) {
            pPosX -= thisRight - contentFrameRight;
        }
        if (thisTop < contentFrameTop) {
            pPosY += contentFrameTop - thisTop;
        } else if (thisBottom > contentFrameBottom) {
            pPosY -= thisBottom - contentFrameBottom;
        }

        if (this.pIsMouseDown) {
            // set position of element
            this.pPosX = pPosX;
            this.pPosY = pPosY;
            $(this.rSelector).css("left", pPosX * this.pScale + "px");
            $(this.rSelector).css("top", pPosY * this.pScale + "px");
        }
    };

    /**
     * Setzt die Letzte Position
     * @param {float} pPosX X-Position
     * @param {float} pPosY Y-Position
     */
    this.setLastPos = function (pPosX, pPosY) {
        this.pLastPosX = pPosX;
        this.pLastPosY = pPosY;
    };

    /**
     * Gibt an ob ein Punkt im Polygon der Hitmap ist
     * @param {[x,y]} point Punkt
     * @param {[[x1,y1], ... , [xN, yN]]} vs Punkte des Polygons
     * @returns {boolean} true falls Punkt in Polygon liegt. Sont false.
     */
    this.isInside = function (point, vs) {
        var x = point[0],
            y = point[1];

        var inside = false;
        for (var i = 0, j = vs.length - 1; i < vs.length; j = i++) {
            var xi = vs[i][0],
                yi = vs[i][1];
            var xj = vs[j][0],
                yj = vs[j][1];

            var intersect = yi > y != yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi;
            if (intersect) inside = !inside;
        }

        return inside;
    };

    /**
     * Funktion wenn ein mousedown-Event stattfindet
     * @param {event} e
     */
    this.mousedown = function (e) {
        this.pMouseOffsetX = e.clientX - $(this.rSelector).offset().left;
        this.pMouseOffsetY = e.clientY - $(this.rSelector).offset().top;

        // set position in Desination free
        if (this.pPosition != null) {
            this.rCurrentDest.free(this.pPosition);
            this.pPosition = null;
            this.rCurrentDest = null;
        }

        if (true) {
            this.pIsMouseDown = true;
            this.setLastPos(e.clientX, e.clientY);
            //$(this.rSelector).appendTo(this.rPatent);
            //$(this.rBox.rSelector).appendTo(this.rBox.rPatent);
            this.setZ(100);
        } else {
            e.preventDefault();
        }

        $(this.rSelector).css("background-color", "rgba(0,0,0,0)");
    };

    /**
     * Funktion wenn ein mouseup-Event stattfindet
     * @param {event} e
     */
    this.mouseup = function (e) {
        if (this.pIsMouseDown) {
            var mouseX = (e.clientX - $(this.targetElement).offset().left) / this.pScale;
            var mouseY = (e.clientY - $(this.targetElement).offset().top) / this.pScale;

            // switch box check

            // 1.: check if outside current box
            // if ($(this.rSelector).offset().left < $(this.rBox.rSelector).offset().left || $(this.rSelector).offset().left > $(this.rBox.rSelector).offset().left + parseFloat($(this.rBox.rSelector).css("width")) || $(this.rSelector).offset().top < $(this.rBox.rSelector).offset().top || $(this.rSelector).offset().top > $(this.rBox.rSelector).offset().top + parseFloat($(this.rBox.rSelector).css("height"))) {
            if (e.clientX < $(this.rBox.rSelector).offset().left || e.clientX > $(this.rBox.rSelector).offset().left + parseFloat($(this.rBox.rSelector).css("width")) || e.clientY < $(this.rBox.rSelector).offset().top || e.clientY > $(this.rBox.rSelector).offset().top + parseFloat($(this.rBox.rSelector).css("height"))) {
                // check for all other boxes
                for (let i = 0; i < boards[activeBoard].rObjects.length; i++) {
                    if (boards[activeBoard].rObjects[i].pType == "Box") {
                        var newBox = boards[activeBoard].rObjects[i];
                        // if (parseFloat($(this.rSelector).offset().left) < parseFloat($(newBox.rSelector).offset().left) + parseFloat($(newBox.rSelector).css("width")) && parseFloat($(this.rSelector).offset().left) >= parseFloat($(newBox.rSelector).offset().left)) {
                        //     if (parseFloat($(this.rSelector).offset().top) < parseFloat($(newBox.rSelector).offset().top) + parseFloat($(newBox.rSelector).css("height")) && parseFloat($(this.rSelector).offset().top) >= parseFloat($(newBox.rSelector).offset().top)) {
                        // check if inside new box on x axis
                        if (e.clientX < parseFloat($(newBox.rSelector).offset().left) + parseFloat($(newBox.rSelector).css("width")) && e.clientX >= parseFloat($(newBox.rSelector).offset().left)) {
                            // check if inside new box on y axis
                            if (e.clientY < parseFloat($(newBox.rSelector).offset().top) + parseFloat($(newBox.rSelector).css("height")) && e.clientY >= parseFloat($(newBox.rSelector).offset().top)) {
                                // is in new box
                                $(this.rSelector).appendTo($(newBox.rSelector));
                                this.rPatent = $(newBox.rSelector);

                                this.setPos((e.clientX - this.pMouseOffsetX - parseFloat($(newBox.rSelector).offset().left)) / this.pScale, (e.clientY - this.pMouseOffsetY - parseFloat($(newBox.rSelector).offset().top)) / this.pScale);
                                this.rBox = newBox;
                                //this.rPatent = newBox;
                            }
                        }
                    }
                }
            }

            // snap to target position
            // set position in Destination free
            if (this.pPosition != null) {
                this.rCurrentDest.free(this.pPosition);
                this.pPosition = null;
                this.rCurrentDest = null;
            }
            this.pFixed = false;
            this.pSolved = false;
            var targetX = parseFloat($(this.targetElement).css("left")) / this.pScale;
            var targetY = parseFloat($(this.targetElement).css("top")) / this.pScale;

            // check if in hitbox of any destination
            for (const dest of this.rDestinations) {
                if (this.rBox == dest.rDestinationBox) {
                    if (this.isInside([mouseX, mouseY], dest.rDropZone)) {
                        this.pPosition = dest.next();

                        if (this.pPosition != null) {
                            this.pFixed = true;
                            this.rCurrentDest = dest;
                        }
                    }
                }
            }

            // check if fixed at a point
            if (this.pFixed) {
                this.setPos(this.pPosition[0] + targetX, this.pPosition[1] + targetY);
            }

            // check if solved
            if (this.rBox == this.rDestination.rDestinationBox) {
                for (const pos of this.rDestination.pDestinationPositions) {
                    if (pos[0] + targetX == this.pPosX && pos[1] + targetY == this.pPosY) {
                        this.pSolved = true;
                        this.isSolved();
                    }
                }
            }

            this.setZ(3);

            this.isSolved();
        }
        this.pIsMouseDown = false;
    };

    /**
     * Funktion wenn ein mousemove-Event stattfindet
     * @param {event} e
     */
    this.mousemove = function (e) {
        if (this.pIsMouseDown) {
            var deltaX = this.pLastPosX - e.clientX;
            var deltaY = this.pLastPosY - e.clientY;
            this.setPos(this.pPosX - deltaX / this.pScale, this.pPosY - deltaY / this.pScale);
            this.setLastPos(e.clientX, e.clientY);
        }
    };

    /**
     * Erstellt die HTML Elemente des Objekts
     * @param {jqueryelement} rParent
     */
    this.create = function (rParent) {
        this.rPatent = rParent;
        this.rSelector = $('<div class="assignmentPiece shadow"></div>').appendTo(this.rPatent);
        makeUnselectable(this.rSelector);

        $(this.rSelector).css("width", this.pWidth * this.pScale + "px");
        $(this.rSelector).css("height", this.pHeight * this.pScale + "px");
        $(this.rSelector).css("left", this.pPosX * this.pScale + "px");
        $(this.rSelector).css("top", this.pPosY * this.pScale + "px");

        // add text element if text piece
        if (this.pText != null) {
            this.rText = $("<span>" + this.pText + "</span>").appendTo($(this.rSelector));
        }

        // add dropText element if text piece
        if (this.pDropText != null) {
            this.rDropText = $("<span>" + this.pDropText + "</span>").appendTo($(this.rSelector));
        }

        // add image element if img piece
        if (this.pImg != null) {
            this.rImg = $('<img src="' + this.pImg + '">').appendTo($(this.rSelector));
            $(this.rSelector).css("box-shadow", "none");
        }

        var t = this;
        // mouse input
        $(this.rSelector).mousedown(function (e) {
            t.mousedown(e);
        });
        $("#body").mouseup(function (e) {
            t.mouseup(e);
        });
        $("#contentFrame").mousemove(function (e) {
            t.mousemove(e);
        });
        // $("#contentFrame").mouseout(function (e) {
        //     t.pIsMouseDown = false;
        //     this.pFixed = false;
        //     this.pSolved = false;
        //     t.isSolved();
        // });

        // touch input
        $(this.rSelector).on("touchstart", function (e) {
            e.preventDefault();
            var touch = e.originalEvent.touches[0] || e.originalEvent.changedTouches[0];
            e.clientX = touch.pageX;
            e.clientY = touch.pageY;
            t.mousedown(e);
        });
        $("#body").on("touchend", function (e) {
            // e.preventDefault();
            var touch = e.originalEvent.touches[0] || e.originalEvent.changedTouches[0];
            e.clientX = touch.pageX;
            e.clientY = touch.pageY;
            t.mouseup(e);
        });

        $("#contentFrame").on("touchmove", function (e) {
            e.preventDefault();
            var touch = e.originalEvent.touches[0] || e.originalEvent.changedTouches[0];
            e.clientX = touch.pageX;
            e.clientY = touch.pageY;
            t.mousemove(e);
        });

        this.reset();
        this.setZ(this.pZ);
    };

    /**
     * Setzt ein Bild als das Aussehen des Teils
     * @param {String} pImg Pfad des Bildes
     */
    this.setImage = function (pImg) {
        this.pImg = pImg;
    };

    /**
     * Setzt einen Text als das Aussehen des Teils
     * @param {String} pText
     */
    this.setText = function (pText) {
        this.pText = pText;
    };

    /**
     * Setze den dropText
     * @param {String} pDropText
     * @param {float} pDropFontSize
     * @param {float} pWDropWidth
     * @param {float} pDropHeight
     */
    this.setDropText = function (pDropText, pDropFontSize, pDropWidth, pDropHeight) {
        this.pDropText = pDropText;
        this.pDropFontSize = pDropFontSize;
        this.pDropWidth = pDropWidth;
        this.pDropHeight = pDropHeight;
    };

    /**
     * Setzt die Schriftgröße des Textes
     * @param {float} pFontSize
     */
    this.setFontSize = function (pFontSize) {
        this.pFontSize = pFontSize;
    };

    /**
     * Setzt die Skalierung des Teils wenn es in einer Gültigen Löseposition ist
     * @param {flaot} pDropScale
     */
    this.setDropScale = function (pDropScale) {
        this.pDropScale = pDropScale;
    };

    /**
     * Setzt die Skalierung
     * @param {float} pScale
     */
    this.setScale = function (pScale) {
        this.pScale = pScale;

        $(this.rSelector).css("width", this.pWidth * this.pScale + "px");
        $(this.rSelector).css("height", this.pHeight * this.pScale + "px");
        $(this.rSelector).css("left", this.pPosX * this.pScale + "px");
        $(this.rSelector).css("top", this.pPosY * this.pScale + "px");
        $(this.rSelector).css("font-size", this.pFontSize * this.pScale + "px");
        $(this.rSelector).css("border-radius", this.pHeight * this.pScale * 0.2 + "px");

        // if (this.pFixed) {
        //     $(this.rSelector).css("width", this.pWidth * this.pScale * this.pDropScale + "px");
        //     $(this.rSelector).css("height", this.pHeight * this.pScale * this.pDropScale + "px");
        //     $(this.rSelector).css("font-size", this.pFontSize * this.pScale * this.pDropScale + "px");
        //     $(this.rSelector).css("border-radius", this.pHeight * this.pScale * this.pDropScale * 0.2 + "px");
        // }

        // adjust stuff for scale and account for Drop specifics
        if (this.pFixed) {
            $(this.rSelector).css("width", this.pWidth * this.pScale * this.pDropScale + "px");
            $(this.rSelector).css("height", this.pHeight * this.pScale * this.pDropScale + "px");
            $(this.rSelector).css("font-size", this.pFontSize * this.pScale * this.pDropScale + "px");
            $(this.rSelector).css("border-radius", this.pHeight * this.pScale * this.pDropScale * 0.2 + "px");
            // show / hide text / dropText
            if (this.pText != null && this.pDropText != null) {
                $(this.rText).hide();
                $(this.rDropText).show();
                // optional dropText adjustments
                if (this.pDropWidth != null) {
                    $(this.rSelector).css("width", this.pDropWidth * this.pScale + "px");
                }
                if (this.pDropHeight != null) {
                    $(this.rSelector).css("height", this.pDropHeight * this.pScale + "px");
                }
                if (this.pDropFontSize != null) {
                    $(this.rSelector).css("font-size", this.pDropFontSize * this.pScale + "px");
                }
            }
        } else {
            // show / hide text / dropText
            if (this.pText != null && this.pDropText != null) {
                $(this.rText).show();
                $(this.rDropText).hide();
            }
        }

        $(this.rText).css("top", (parseFloat($(this.rSelector).css("height")) - parseFloat($(this.rText).css("height"))) / 2);
        $(this.rText).css("left", (parseFloat($(this.rSelector).css("width")) - parseFloat($(this.rText).css("width"))) / 2);
        $(this.rDropText).css("top", (parseFloat($(this.rSelector).css("height")) - parseFloat($(this.rDropText).css("height"))) / 2);
        $(this.rDropText).css("left", (parseFloat($(this.rSelector).css("width")) - parseFloat($(this.rDropText).css("width"))) / 2);

        //this.isSolved();
    };

    /**
     * Stellt den Loesungszustand her
     */
    this.solve = function () {
        this.pSolved = true;
        this.pIsMouseDown = false;
        this.pFixed = true;

        this.rBox = this.rDestination.rDestinationBox;
        $(this.rSelector).appendTo($(this.rBox.rSelector));
        this.rParent = this.rDestination.rDestinationBox.rSelector;

        var targetX = parseFloat($(this.targetElement).css("left")) / this.pScale;
        var targetY = parseFloat($(this.targetElement).css("top")) / this.pScale;

        var dest = this.rDestination;
        this.pPosition = dest.next();

        if (this.pPosition != null) {
            this.pFixed = true;
            this.rCurrentDest = dest;
            this.setPos(this.pPosition[0] + targetX, this.pPosition[1] + targetY);
        } else {
            console.log("ERROR: pPosition = null");
        }

        this.isSolved();
    };

    /**
     * Gibt an ob das Assignment korrekt gelöst ist
     * @returns {boolean}
     */
    this.isSolved = function () {
        var s = true;
        for (let i = 0; i < this.rPieces.length; i++) {
            s = s && this.rPieces[i].pFixed;
        }

        // update colors on other pieces if checkWhenDone
        if (s != this.pAllSolved) {
            this.pAllSolved = s;
            for (let i = 0; i < this.rPieces.length; i++) {
                if (this.rPieces[i] != this) this.rPieces[i].isSolved();
            }
            for (let i = 0; i < this.rPieces.length; i++) {
                if (this.rPieces[i] != this) this.rPieces[i].isSolved();
            }
        }

        if ((!this.pCheckWhenDone || (this.pCheckWhenDone && s)) && this.pFixed) {
            // fit size to text when solved
            //this.pWidth = parseFloat($(this.rSelector).find("span").first().css("width")) * 2;

            if (this.pSolved) {
                $(this.rSelector).css("background-color", this.color_solved);
                // images
                if (this.rImg != null) {
                    $(this.rSelector).css("background-color", "transparent");
                    $(this.rImg).removeClass("alphaImgOutlineRed");
                    $(this.rImg).addClass("alphaImgOutlineGreen");
                }
            } else {
                $(this.rSelector).css("background-color", this.color_unsolved);
                // images
                if (this.rImg != null) {
                    $(this.rSelector).css("background-color", "transparent");
                    $(this.rImg).removeClass("alphaImgOutlineGreen");
                    $(this.rImg).addClass("alphaImgOutlineRed");
                }
            }
        } else {
            $(this.rSelector).css("background-color", this.pColor);
            // images
            if (this.rImg != null) {
                $(this.rSelector).css("background-color", "transparent");
                $(this.rImg).removeClass("alphaImgOutlineGreen");
                $(this.rImg).removeClass("alphaImgOutlineRed");
            }

            // set size to normal
            this.pWidth = this.pInitialWidth;
        }

        this.setScale(this.pScale);
        return this.pSolved;
    };

    /**
     * Aktualisiert die Liste der anderen Teile
     * @param {AssignmentPiece[]} rPieces
     */
    this.setPieces = function (rPieces) {
        this.rPieces = rPieces;
    };

    /**
     * Stellt den Ausganszustand wieder her
     */
    this.reset = function () {
        $(this.rSelector).appendTo($(this.rStartBox.rSelector));
        this.rBox = this.rStartBox;
        this.rParent = $(this.rStartBox.rSelector);
        this.setPos(this.pStartPosX, this.pStartPosY);
        this.pSolved = false;
        this.pFixed = false;

        // reset slot in destination
        if (this.rCurrentDest != null) this.rCurrentDest.free(this.pPosition);
        this.pPosition = null;
        this.rCurrentDest = null;

        var t = this;
        $(this.rSelector).unbind("mousedown");
        $(this.rSelector).mousedown(function (e) {
            t.mousedown(e);
        });
        $(this.rSelector).unbind("mouseup");
        $(this.rSelector).mouseup(function (e) {
            t.mouseup(e);
        });
        $(this.rSelector).unbind("mousemove");
        $(this.rSelector).mousemove(function (e) {
            t.mousemove(e);
        });
        $(this.rSelector).unbind("touchstart");
        $(this.rSelector).on("touchstart", function (e) {
            e.preventDefault();
            var touch = e.originalEvent.touches[0] || e.originalEvent.changedTouches[0];
            e.clientX = touch.pageX;
            e.clientY = touch.pageY;
            t.mousedown(e);
        });
        $(this.rSelector).unbind("touchend");
        $(this.rSelector).on("touchend", function (e) {
            e.preventDefault();
            var touch = e.originalEvent.touches[0] || e.originalEvent.changedTouches[0];
            e.clientX = touch.pageX;
            e.clientY = touch.pageY;
            t.mouseup(e);
        });
        $(this.rSelector).unbind("touchmove");
        $("#contentFrame").on("touchmove", function (e) {
            e.preventDefault();
            var touch = e.originalEvent.touches[0] || e.originalEvent.changedTouches[0];
            e.clientX = touch.pageX;
            e.clientY = touch.pageY;
            t.mousemove(e);
        });
        this.pIsMouseDown = false;
        $(this.rSelector).css("background-color", this.pColor);

        this.isSolved();
    };

    /**
     * Speichert den Zustand in einem JSON-Objekt
     * @returns {JSON} Zustand
     */
    this.export = function () {
        var obj = {
            pJsonId: this.pJsonId,
            pType: this.pType,
            pSolved: this.pSolved,
            pFixed: this.pFixed,
            pPosX: this.pPosX,
            pPosY: this.pPosY,
            rBox: this.rBox.pJsonId,
            rDestination: this.rDestination.pJsonId,
        };

        return obj;
    };

    /**
     * Setzt den in einem JSON-Objekt um
     * @param {JSON} obj
     */
    this.import = function (rObj) {
        if (rObj.pType == this.pType && rObj.pJsonId == this.pJsonId) {
            var parentBox;
            block: {
                for (let i in boards) {
                    for (let j in boards[i].rObjects) {
                        if (boards[i].rObjects[j].pType == "Box") {
                            var importedBox = boards[i].rObjects[j];
                            if (boards[i].rObjects[j].pJsonId == rObj.rBox) {
                                $(this.rSelector).appendTo($(importedBox.rSelector));
                                this.rPatent = $(importedBox.rSelector);
                                this.rBox = importedBox;
                                break block;
                            }
                        }
                    }
                }
            }
            this.setPos(rObj.pPosX, rObj.pPosY);
            this.pFixed = rObj.pFixed;
            this.pSolved = rObj.pSolved;
            this.isSolved();
            for (const dest of this.rDestinations) {
                if (dest.pJsonId == rObj.rDestination) {
                    this.rCurrentDest = dest;
                }
            }
            this.pPosition = [rObj.pPosX, rObj.pPosY];
            this.rCurrentDest.remove(this.pPosition);
        } else {
            console.log("ERROR importing puzzlePiece");
        }
    };

    /**
     * Wechselt die Box des Teils
     * @param {Box} newBox
     */
    this.switchBox = function (newBox) {};

    /**
     * Setzt den css Z-Index des Teils
     * @param {int} pZ Z-index
     */
    this.setZ = function (pZ) {
        $(this.rSelector).css("z-index", pZ);
        this.pZ = pZ;
    };

    /**
     * Setzt die Farbe des Teils
     * @param {RGB (r,g,b)} pColor
     */
    this.setColor = function (pColor) {
        this.pColor = pColor;
    };

    /**
     * Stellt ein wann die Teile als korrekt/falsch eingefärbt werden sollen.
     * Bei "True" werden alle Teile gemeinsam als korrekt/falsch markiert, wenn alle Teile zugeordnet wurden.
     * Bei "False" wird jedes Teil direkt nach dem zuordnen als korrekt/falsch markiert
     * @param {boolean} pCheckWhenDone
     */
    this.checkWhenDone = function (pCheckWhenDone) {
        this.pCheckWhenDone = pCheckWhenDone;
        this.isSolved();
    };
}

/**
 * Klasse Assignment
 * Ermöglicht Texte/Bilder in Gruppen und Regionen mit mehreren einrastbaren Slots einzuordnen
 *
 * @param pTitle {String} Titel
 * @param pMaxDist {float} Maximale Abweichung bei der Zuordnung
 */
function Assignment(pTitle, pMaxDist) {
    this.pTitle = pTitle;
    this.pType = "Assignment";
    this.pSolved = false;
    this.rPieces = [];
    this.targets = [];
    this.pMaxDist = pMaxDist;
    this.pJsonId = newJsonId();
    this.TEMP_POLY = [];
    this.rDestinations = new Set();
    this.pCheckWhenDone = false;
    this.pDropScale = 0.6;

    /**
     * Fuegt ein Hintergrundbild hinzu
     * @param {string} pImgSrc
     * @param {float} pPosX
     * @param {float} pPosY
     * @param {float} pWidth
     * @param {float} pHeight
     * @param {Box} rBox
     * @param {Bool} pShowCanvas wenn true werden die Hitmaps in enem Canvas gezeichnet
     * @param {float} pBorderRadius Abrundung der Ecken
     */
    this.addBackgroundImage = function (pImgSrc, pPosX, pPosY, pWidth, pHeight, rBox, pShowCanvas, pBorderRadius) {
        this.background = $('<img src="' + pImgSrc + '" class="puzzleBackground">').prependTo(rBox.rSelector);
        $(this.background).css("width", pWidth + "px");
        $(this.background).css("height", pHeight + "px");
        $(this.background).css("left", pPosX + "px");
        $(this.background).css("top", pPosY + "px");
        $(this.background).attr("initialPosX", pPosX);
        $(this.background).attr("initialPosY", pPosY);
        $(this.background).attr("pInitialWidth", pWidth);
        $(this.background).attr("pInitialHeight", pHeight);
        $(this.background).attr("draggable", false);

        if (pBorderRadius != null) {
            $(this.background).css("border-radius", pBorderRadius + "px");
            $(this.background).attr("pInitialBorder-radius", pBorderRadius);
        } else {
            $(this.background).css("border-radius", 0 + "px");
            $(this.background).attr("pInitialBorder-radius", 0);
        }

        // create canvas
        if (pShowCanvas) {
            this.canvas = $('<canvas class="assignmentCanvas" width="' + pWidth + '" height="' + pHeight + '"></canvas>').prependTo(rBox.rSelector);
            $(this.canvas).css("width", pWidth + "px");
            $(this.canvas).css("height", pHeight + "px");
            $(this.canvas).css("left", pPosX + "px");
            $(this.canvas).css("top", pPosY + "px");
            $(this.canvas).attr("initialPosX", pPosX);
            $(this.canvas).attr("initialPosY", pPosY);
            $(this.canvas).attr("pInitialWidth", pWidth);
            $(this.canvas).attr("pInitialHeight", pHeight);
            $(this.canvas).attr("draggable", false);

            // draw hitmaps on canvas
            var canvas = $(this.canvas)[0];
            var ctx = canvas.getContext("2d");

            for (let i = 0; i < this.rPieces.length; i++) {
                var hitmap = this.rPieces[i].rDestination.rDropZone;
                if (hitmap != null) {
                    var colors = ["red", "green", "blue", "black", "yellow", "white"];
                    ctx.beginPath();
                    ctx.strokeStyle = colors[Math.round(Math.random() * colors.length)];
                    for (let j = 0; j < hitmap.length; j++) {
                        var x = hitmap[j][0];
                        var y = hitmap[j][1];
                        if (j > 0) {
                            ctx.lineTo(x, y);
                        } else {
                            ctx.moveTo(x, y);
                        }
                    }
                    ctx.closePath();
                    ctx.stroke();
                    ctx.fillStyle = ctx.strokeStyle + "40";
                    ctx.fill();
                }
            }
        }

        for (let i = 0; i < this.rPieces.length; i++) {
            this.rPieces[i].targetElement = this.background;
        }

        var t = this;

        // print out hitmap (for creating hitmaps)
        if (this.pLogHitmap) {
            $(this.background).mousedown(function (e) {
                var mX = (e.clientX - $(t.background).offset().left) / t.pScale;
                var mY = (e.clientY - $(t.background).offset().top) / t.pScale;
                t.TEMP_POLY.push([mX, mY]);
                var logHitmap = "[";
                for (let i = 0; i < t.TEMP_POLY.length; i++) {
                    if (i != 0) {
                        logHitmap = logHitmap + ",";
                    }
                    logHitmap = logHitmap + "[" + Math.round(t.TEMP_POLY[i][0]) + "," + Math.round(t.TEMP_POLY[i][1]) + "]";
                }
                logHitmap = logHitmap + "]";

                console.log(logHitmap);
            });
        }
    };

    /**
     *
     * @param {Box} rDestinationBox
     * @param {Polygon [[x1,x2], ... , [xn, yn]]} rDropZone Array von zweistelligen Arrays die die Punkte in einem Polygon darstellen
     * @param {Punkte [[x1,x2], ... , [xn, yn]]} pDestinationPositions Die Positionen in die die Teile einrasten
     * @returns {AssignmentDestination} Referenz auf das neue Ziel
     */
    this.addDestination = function (rDestinationBox, rDropZone, pDestinationPositions) {
        var dest = new AssignmentDestination(rDestinationBox, rDropZone, pDestinationPositions);
        this.rDestinations.push(dest);
        return dest;
    };

    /**
     * Fügt ein Teil in Form eines Bildes hinzu
     * @param {string} pImg
     * @param {Box} rStartBox
     * @param {float} pStartPosX
     * @param {float} pStartPosY
     * @param {float} pWidth
     * @param {float} pHeight
     * @param {AssignmentDestination} rDestination Zielinformation
     * @returns {AssignmentPiece} Referenz auf das neue Teil
     */
    this.addImagePiece = function (pImg, rStartBox, pStartPosX, pStartPosY, pWidth, pHeight, rDestination) {
        var imgPiece = new AssignmentPiece(pStartPosX, pStartPosY, rStartBox, pWidth, pHeight, rDestination);
        imgPiece.setImage(pImg);
        imgPiece.setPieces(this.rPieces);
        imgPiece.checkWhenDone(this.pCheckWhenDone);
        imgPiece.setDropScale(this.pDropScale);
        //textPiece
        this.rDestinations.add(rDestination);
        this.rPieces.push(imgPiece);

        for (let i = 0; i < this.rPieces.length; i++) {
            this.rPieces[i].rDestinations = this.rDestinations;
        }

        return imgPiece;
    };

    /**
     * Fügt ein Teil in Form eines Textes hinzu
     * @param {string} pText
     * @param {box} rStartBox
     * @param {float} pStartPosX
     * @param {float} pStartPosY
     * @param {float} pWidth
     * @param {float} pHeight
     * @param {AssignmentDestination} rDestination Zielinformation
     * @returns {AssignmentPiece} Referenz auf das neue Teil
     */
    this.addTextPiece = function (pText, rStartBox, pStartPosX, pStartPosY, pWidth, pHeight, rDestination) {
        var textPiece = new AssignmentPiece(pStartPosX, pStartPosY, rStartBox, pWidth, pHeight, rDestination);
        textPiece.setColor("rgb(0,100,255)");
        textPiece.setText(pText);
        textPiece.setFontSize(10);
        textPiece.setPieces(this.rPieces);
        textPiece.checkWhenDone(this.pCheckWhenDone);
        textPiece.setDropScale(this.pDropScale);
        //textPiece
        this.rDestinations.add(rDestination);
        this.rPieces.push(textPiece);

        for (let i = 0; i < this.rPieces.length; i++) {
            this.rPieces[i].rDestinations = this.rDestinations;
        }

        return textPiece;
    };

    /**
     * Fuegt ein neues Teil hinzu
     * @param {string} pText
     * @param {float} pWidth
     * @param {float} pHeight
     * @param {float} pFontSize
     * @param {color} pColor
     * @param {Box} rStartBox
     * @param {float} pStartPosX
     * @param {float} pStartPosY
     * @param {Box} rDestinationBox
     * @param {float} pDestinationPosX
     * @param {float} pDestinationPosY
     * @param {Polygon [[x1,x2], ... , [xn, yn]]} rDestinationHitmap Hitmap Polygon
     */
    this.addPiece = function (pText, pWidth, pHeight, pFontSize, pColor, rStartBox, pStartPosX, pStartPosY, rDestinationBox, pDestinationPosX, pDestinationPosY, rDestinationHitmap) {
        var dest = new AssignmentDestination(rDestinationBox, rDestinationHitmap, [[pDestinationPosX, pDestinationPosY]]);
        var piece = this.addTextPiece(pText, rStartBox, pStartPosX, pStartPosY, pWidth, pHeight, dest);
        piece.setFontSize(pFontSize);
        piece.setColor(pColor);
    };

    /**
     * Erstellt die HTML Elemente des Objektes
     * @param {jqueryelement} rParent
     */
    this.create = function (rParent) {
        this.rPatent = rParent;
        //this.rSelector = $('<div class="puzzle"></div>').appendTo(this.rPatent);
        //makeUnselectable(this.rSelector);
        for (let i = 0; i < this.rPieces.length; i++) {
            this.rPieces[i].create(this.rPatent);
        }

        // individual solve button
        var btns = boards[this.pBoardId].addSolvabel(this);
        this.rSolve = btns.solve;
        this.rReset = btns.reset;
    };

    /**
     * Setzt das Icon fuer den Solve-Knopf
     * @param {string} pIcon Pfad zum Icon-Bild
     */
    this.setSolveIcon = function (pIcon) {
        this.rSolve.css("background-image", "url(" + pIcon + ")");
    };

    /**
     * Setzt das Icon fuer den Reset-Knopf
     * @param {string} pIcon Pfad zum Icon-Bild
     */
    this.setResetIcon = function (pIcon) {
        this.rReset.css("background-image", "url(" + pIcon + ")");
    };

    /**
     * Setzt die Skalierung aller Teile wenn sie in einer gültigen Löseposition sind
     * @param {float} pDropScale
     */
    this.setDropScale = function (pDropScale) {
        this.pDropScale = pDropScale;
    };

    this.logHitmap = function () {
        this.pLogHitmap = true;
    };

    /**
     * Setzt die Skalierung
     * @param {float} pScale
     */
    this.setScale = function (pScale) {
        this.pScale = pScale;

        $(this.background).css("left", parseFloat($(this.background).attr("initialPosX")) * this.pScale + "px");
        $(this.background).css("top", parseFloat($(this.background).attr("initialPosY")) * this.pScale + "px");
        $(this.background).css("width", parseFloat($(this.background).attr("pInitialWidth")) * this.pScale + "px");
        $(this.background).css("height", parseFloat($(this.background).attr("pInitialHeight")) * this.pScale + "px");
        $(this.background).css("border-radius", parseFloat($(this.background).attr("pInitialBorder-radius")) * this.pScale + "px");
        for (let i = 0; i < this.rPieces.length; i++) {
            this.rPieces[i].setScale(this.pScale);
        }

        // canvas
        $(this.canvas).css("left", parseFloat($(this.canvas).attr("initialPosX")) * this.pScale + "px");
        $(this.canvas).css("top", parseFloat($(this.canvas).attr("initialPosY")) * this.pScale + "px");
        $(this.canvas).css("width", parseFloat($(this.canvas).attr("pInitialWidth")) * this.pScale + "px");
        $(this.canvas).css("height", parseFloat($(this.canvas).attr("pInitialHeight")) * this.pScale + "px");
    };

    /**
     * Stellt den Ausganszustand wieder her
     */
    this.reset = function () {
        for (let i = 0; i < this.rPieces.length; i++) {
            this.rPieces[i].reset();
        }
        for (const dest of this.rDestinations) {
            dest.reset();
        }
    };

    /**
     * Stellt den Loesungszustand her
     */
    this.solve = function () {
        for (let i = 0; i < this.rPieces.length; i++) {
            if (!this.rPieces[i].pSolved) this.rPieces[i].reset();
        }
        for (let i = 0; i < this.rPieces.length; i++) {
            if (!this.rPieces[i].pSolved) this.rPieces[i].solve();
        }
        this.pSolved = true;
    };

    /**
     * Gibt an ob der Lösungszustand vorliegt
     * @returns {boolean}
     */
    this.isSolved = function () {
        var s = true;
        for (let i = 0; i < this.rPieces.length; i++) {
            if (!this.rPieces[i].isSolved()) {
                s = false;
            }
        }
        this.pSolved = s;
        return this.pSolved;
    };

    /**
     * Stellt ein wann die Teile als korrekt/falsch eingefärbt werden sollen.
     * Bei "True" werden alle Teile gemeinsam als korrekt/falsch markiert, wenn alle Teile zugeordnet wurden.
     * Bei "False" wird jedes Teil direkt nach dem zuordnen als korrekt/falsch markiert
     * @param {boolean} pCheckWhenDone
     */
    this.checkWhenDone = function (pCheckWhenDone) {
        this.pCheckWhenDone = pCheckWhenDone;

        for (let i = 0; i < this.rPieces.length; i++) {
            this.rPieces[i].checkWhenDone(pCheckWhenDone);
        }
    };

    /**
     * Speichert den Zustand in einem JSON-Objekt
     * @returns {JSON} Zustand
     */
    this.export = function () {
        var obj = {
            pJsonId: this.pJsonId,
            pType: this.pType,
            rPieces: [],
            pSolved: this.pSolved,
        };

        for (let i = 0; i < this.rPieces.length; i++) {
            obj.rPieces.push(this.rPieces[i].export());
        }

        return obj;
    };

    /**
     * Setzt den in einem JSON-Obbjekt gespeicherten Zustand um
     * @param {JSON} rObj
     */
    this.import = function (rObj) {
        this.reset();
        if ((rObj.pType = this.pType && rObj.pJsonId == this.pJsonId)) {
            this.pSolved = rObj.pSolved;
            for (let i = 0; i < rObj.rPieces.length; i++) {
                this.rPieces[i].import(rObj.rPieces[i]);
            }
        } else {
            console.log("ERROR importing Assignment");
        }
    };
}
