// requires acanvas.js and 8th.js also
var INSIDE_AT_OUTSIDE_COLORS = ["#0a0", "#e8d500", "#f00"];
var LEVELS_H = [10, 30, 60, 120, 180, 220, 300];
// - 1st is 10 instead 0 for better visibility of 3 red bases on the red level background
var DEFAULT_TRANSPARENT_HIGHLIGHTING_ALPHA = 0.6;

var _flag = 1;
var FLAG_SLOPING = _flag;
var FLAG_ALSO_LITTLE_ADDITIONAL_EDGES = (_flag *= 2);
var FLAG_INSIDE_AT_OUTSIDE = (_flag *= 2);
var FLAG_SKIP_KEY_POINTS_10 = (_flag *= 2);
var FLAG_SKIP_KEY_POINTS = (_flag *= 2);
// - first 5 flags cannot be used together with rotatingTriangle() function: see TRIANGLE_FLAG_MASK
var FLAG_SKIP_LINES = (_flag *= 2);
var FLAG_SKIP_NON_HORIZONTAL_LINES = (_flag *= 2);
var FLAG_NO_DASH = (_flag *= 2);
var FLAG_ALWAYS_DASH = (_flag *= 2);
var FLAG_THIN = (_flag *= 2);
var FLAG_HIGH_TETRAHEDRON_FIRST = (_flag *= 2)
var FLAG_SYMMETRY = (_flag *= 2);
var FLAG_LARGE_BASES = (_flag *= 2);
var FLAG_LARGE_BASES_SHAMAIM = (_flag *= 2);
var FLAG_LARGE_YETZER_LEV = (_flag *= 2);
var FLAG_LARGE_AV_VE = (_flag *= 2);
var FLAG_DIFFERENT_BASES = (_flag *= 2);
var FLAG_VERTICAL_AXIS = (_flag *= 2);
var FLAG_LEFT_LEVELS = (_flag *= 2);
var FLAG_ARROW_FOR_LEVEL = (_flag *= 2);
var FLAG_ARROW_ONLY_FOR_1_5_2 = (_flag *= 2);
var FLAG_LITTLE_KEY_POINTS = (_flag *= 2);
var FLAG_LITTLE_BLACK_KEY_POINTS = (_flag *= 2);
var FLAG_VERY_LITTLE_KEY_POINTS = (_flag *= 2);
var FLAG_WEAK_KEY_POINTS = (_flag *= 2);
var FLAG_SKIP_SPHERE = (_flag *= 2);
var FLAG_HIGHLIGHT_SPHERE = (_flag *= 2);
var FLAG_REDUCE_HIGHLIGHTING = (_flag *= 2);
var FLAG_EXPAND_HIGHLIGHTING = (_flag *= 2);
var FLAG_EXPAND_HIGHLIGHTING_4 = (_flag *= 2);
var FLAG_TRANSPARENT_HIGHLIGHTING = (_flag *= 2); // - partially transparent

function makeTwoTetrahedrons(context, canvas, day, flags, rotationZ, size) {
    flags = flags ? flags : 0;
    var sloping = flags & FLAG_SLOPING;
    var tetrahedrons = new Tetrahedrons(context);
    tetrahedrons.day = day;
    tetrahedrons.flags = flags;
    tetrahedrons.lineWidth = flags & FLAG_SKIP_LINES ? 0 : flags & FLAG_THIN ? 1 : 3;
    tetrahedrons.coloredLineWidth = flags & FLAG_SKIP_LINES ? 0 : flags & FLAG_THIN ? 5 : 5;
    tetrahedrons.useDash = (flags & FLAG_NO_DASH) == 0;
    tetrahedrons.alwaysDash = (flags & FLAG_ALWAYS_DASH) != 0;
    if (size == null) {
        size = (day == 10 ? 0.7 : canvas.width > canvas.height ? 0.75 : 0.87) * Math.min(canvas.width, canvas.height);
    }
    tetrahedrons.setSize(size);
    tetrahedrons.zHorizontalSections = day <= 1 ? [] : [1.0 / 3.0, 1.0 / 2.0];
    if (day == 10 && !(flags & FLAG_SKIP_KEY_POINTS_10)) {
        tetrahedrons.zHorizontalSections.push(2.0 / 3.0);
    }
    tetrahedrons.color = "#000";
    tetrahedrons.sphereColor = flags & FLAG_HIGHLIGHT_SPHERE ? "#0f0" : "#ccf";
    tetrahedrons.verticalAxisColor = "#080";
    tetrahedrons.rotationAroundX = Math.PI * (sloping ? 5.0 : 5.0) / 180.0;
    tetrahedrons.rotationAroundY = Math.PI * (sloping ? 0.0 : 0.0) / 180.0;
    rotationZ = rotationZ != null ? rotationZ : 7.0;
    tetrahedrons.rotationAroundZ = Math.PI * rotationZ / 180.0;
    return tetrahedrons;
}

function drawTwoTetrahedronsFullConfiguration(context, canvas, day, flags, levelTitle, levelGap,
                                              highlightingLevels, highlightingLevelsAlpha) {
    var tetrahedrons = makeTwoTetrahedrons(context, canvas, day, flags);
    tetrahedrons.levelGap = levelGap ? levelGap : 0;
    tetrahedrons.levelTitle = levelTitle;
    if (highlightingLevels != null) {
        tetrahedrons.highlightingLevels = highlightingLevels;
    }
    if (highlightingLevelsAlpha != null) {
        tetrahedrons.highlightingLevelsAlpha = highlightingLevelsAlpha;
    }
    tetrahedrons.drawFullConfiguration();
}

function drawTwoTetrahedronsWithRotation(context, canvas, day, flags, rotationZ, highlightingLevels) {
    var tetrahedrons = makeTwoTetrahedrons(context, canvas, day, flags, rotationZ);
    if (highlightingLevels != null) {
        tetrahedrons.highlightingLevels = highlightingLevels;
    }
    tetrahedrons.drawFullConfiguration();
}

function Tetrahedrons(context) {
    this.context = context;
    this.canvas = context.canvas;
    this.day = 0;
    this.flags = 0;
    this.edgeLength = 10;
    this.zShift = 5;
    this.lineWidth = 1;
    this.lineWidthForSecond = null;
    this.littleAdditionalLineWidth = 1;
    this.coloredLineWidth = 1;
    this.useDash = true;
    this.alwaysDash = false;
    this.symmetry = false;

    this.needHeight = false;
    this.verticalAxisWidth = 3;
    this.verticalAxisDash = true;
    this.differentBases = false;
    this.differentBasesOnlyFirst3Points = true;

    this.levelGap = 0;
    this.levelTitle = null;
    this.levelLineWidth = 2;
    this.levelTitleWidth = 100;
    this.levelTitleFont = FONT_ON_IMAGE;
    this.arrowForLevel = false;

    this.highlightingLevels = [];
    this.highlightingLevelWidth = 50;
    this.highlightingLevelGapForOverlap = 30;
    this.highlightingLevelExpandingHorizontal = 30;
    this.highlightingLevelExpandingVertical = 4;
    this.highlightingLevelOnlyLow = false;
    this.highlightingLevelOnlyHigh = false;
    this.highlightingLevelColors = [];
    this.highlightingLevelsAlpha = [];

    this.rotationAroundX = 0.0;
    this.rotationAroundY = 0.0;
    this.rotationAroundZ = 0.0;
    this.zHorizontalSections = [];

    this.color = "#000000";
    this.littleAdditionalLineWidthColor = null;
    this.sphereColor = "#000000";
    this.verticalAxisColor = "#000000";
    this.levelLineColor = "#000000";
    this.levelArrowColor = "#c00000";
    this.levelUnimportantLevelColor = "#A0A0A0";

    this.keyPointRadius = 3;
    this.blackKeyPointRadius = 1;
    this.keyPointInsideAtOutsideColors = [];
    this.keyPointLowColors = [];
    this.keyPointHighColors = [];
    this.keyEdgeLowColors = [];
    this.keyEdgeHighColors = [];

    this.drawFullConfiguration = function(x, y) {
        if (x == null) {
            x = this.flags & FLAG_LEFT_LEVELS ?
                (this.canvas.width + this.levelGap) / 2 :
                (this.canvas.width - this.levelGap) / 2;
        }
        if (y == null) {
            y = this.canvas.height / 2;
        }
        this.setSpectralHighlightingColors();
        if ((this.flags & FLAG_SKIP_KEY_POINTS) == 0) {
            this.setDefaultPointColors();
        }
        if (this.highlightingLevels.length > 0) {
            this.highlightLevels(x, y);
        }
        this.drawTwoTetrahedrons(x, y);
        if (this.day >= 7 && (this.flags & FLAG_SKIP_SPHERE) == 0) {
            this.drawShekhinahSphere(x, y);
        }

        var saveLineWidths = [
            this.lineWidth, this.littleAdditionalLineWidth, this.coloredLineWidth, this.verticalAxisWidth
        ];
        this.lineWidth = 0;
        this.littleAdditionalLineWidth = 0;
        this.coloredLineWidth = 0;
        this.verticalAxisWidth = 0;
        // - repeat drawing key points AFTER all lines
        this.blackKeyPointRadius = this.flags & FLAG_VERY_LITTLE_KEY_POINTS ? 2 :
            this.flags & FLAG_LITTLE_KEY_POINTS ? 2 : 4;
        this.keyPointRadius = this.flags & FLAG_VERY_LITTLE_KEY_POINTS ? 2 :
            this.flags & FLAG_LITTLE_KEY_POINTS ? 3 : 5;
        if (this.flags & FLAG_INSIDE_AT_OUTSIDE) {
            this.keyPointInsideAtOutsideColors = INSIDE_AT_OUTSIDE_COLORS;
        }
        this.drawTwoTetrahedrons(x, y);
        if (this.flags & (FLAG_LARGE_BASES | FLAG_LARGE_BASES_SHAMAIM | FLAG_LARGE_AV_VE)) {
            for (var k = 0; k < this.keyPointHighColors.length; k++) {
                var large = false;
                if (this.flags & FLAG_LARGE_BASES_SHAMAIM) {
                    if (k < 3) { // || k == this.keyPointLowColors.length - 1) {
                        large = true;
                    }
                }
                if (this.flags & (FLAG_LARGE_AV_VE | FLAG_LARGE_YETZER_LEV)) {
                    if (k == 18) {
                        large = true;
                    }
                }
                if (!large) {
                    this.keyPointHighColors[k] = null;
                }
            }
            for (var k = 0; k < this.keyPointLowColors.length; k++) {
                var large = false;
                if (this.flags & FLAG_LARGE_BASES) {
                    if (k < 3) { // || k == this.keyPointLowColors.length - 1) {
                        large = true;
                    }
                }
                if (this.flags & FLAG_LARGE_AV_VE) {
                    if (k >= 9 && k < 15) {
                        large = true;
                    }
                }
                if (this.flags & FLAG_LARGE_YETZER_LEV) {
                    if (k == 18) {
                        large = true;
                    }
                }
                if (!large) {
                    this.keyPointLowColors[k] = null;
                    // - removing all other points: we need redraw only enlarged points (2nd pass)
                }
            }
            this.keyPointRadius = (this.flags & FLAG_LITTLE_KEY_POINTS) ? 7 : 10;
            this.differentBases = this.flags & FLAG_DIFFERENT_BASES;
            this.drawTwoTetrahedrons(x, y);
        }
        this.lineWidth = saveLineWidths[0];
        this.littleAdditionalLineWidth = saveLineWidths[1];
        this.coloredLineWidth = saveLineWidths[2];
        this.verticalAxisWidth = saveLineWidths[3];

        if (this.levelGap) {
            this.arrowForLevel = this.flags & FLAG_ARROW_FOR_LEVEL;
            this.levelTitleWidth =
                this.day <= 7 ? 0.9 * this.levelGap :
                this.day == 8 ? 0.9 * this.levelGap / 2 :
                0.9 * this.levelGap / 3;
            var xLevels = this.flags & FLAG_LEFT_LEVELS ? 0 : this.canvas.width - this.levelGap + 0.05 * this.levelGap;
            this.drawLevelsTitles(xLevels, y, this.levelTitle);
        }
    }

    // Axis x is from left to right, axis z is from top to bottom, axis y is from viewer into picture
    this.drawLowTetrahedron = function (x, y) {
        var xyz = this.rotate(tetrahedronVertexes(this.edgeLength, this.zShift, false));
        this.drawTetrahedronImpl(x, y, xyz, true, true);
    }

    this.drawHighTetrahedron = function (x, y) {
        var xyz = this.rotate(tetrahedronVertexes(this.edgeLength, this.zShift, true));
        this.drawTetrahedronImpl(x, y, xyz, false, false);
    }

    this.drawTwoTetrahedrons = function (x, y) {
        if (this.flags & FLAG_VERTICAL_AXIS) {
            this.drawVerticalAxis(x, y);
        }
        var saveLineWidth = this.lineWidth;
        if (this.flags & FLAG_HIGH_TETRAHEDRON_FIRST) {
            this.drawHighTetrahedron(x, y);
            if (this.lineWidthForSecond != null && this.lineWidth) {
                this.lineWidth = this.lineWidthForSecond;
            }
            this.drawLowTetrahedron(x, y);
        } else {
            this.drawLowTetrahedron(x, y);
            if (this.lineWidthForSecond != null && this.lineWidth) {
                this.lineWidth = this.lineWidthForSecond;
            }
            this.drawHighTetrahedron(x, y);
        }
        this.lineWidth = saveLineWidth;
    }

    this.drawVerticalAxis = function (x, y, scale, onlyOneEndIndex) {
        var xyzLow = this.rotate(tetrahedronVertexes(this.edgeLength, this.zShift, false))[3];
        var xyzHigh = this.rotate(tetrahedronVertexes(this.edgeLength, this.zShift, true))[3];
        scale = scale == null ? 1.0 : scale;
        var yEnds = [y + scale * xyzLow.z, y + scale * xyzHigh.z];
        if (onlyOneEndIndex != null) {
            fillCircle(this.context, x, yEnds[onlyOneEndIndex], 0.5 * this.verticalAxisWidth, this.verticalAxisColor);
        } else {
            this.context.strokeStyle = this.verticalAxisColor;
            this.context.lineCap = "round";
            this.context.lineJoin = "round";
            this.context.lineWidth = this.verticalAxisWidth;
            drawLine(this.context, x + xyzHigh.x, yEnds[0], x + xyzLow.x, yEnds[1], this.verticalAxisDash);
            this.context.setLineDash([]);
            // - clearing dash mode (to be on the safe side)
        }
    }

    this.drawLevelsTitles = function (x, y, levelTitle) {
        var xyzLow = this.rotate(tetrahedronVertexes(this.edgeLength, this.zShift, false));
        var xyzHigh = this.rotate(tetrahedronVertexes(this.edgeLength, this.zShift, true));
        this.drawLevelsTitlesImpl(x, y, xyzLow, xyzHigh, levelTitle);
    }

    this.highlightLevels = function (x, y) {
        var xyzLow = this.rotate(tetrahedronVertexes(this.edgeLength, this.zShift, false));
        var xyzHigh = this.rotate(tetrahedronVertexes(this.edgeLength, this.zShift, true));
        this.highlightLevelsImpl(x, y, xyzLow, xyzHigh);
    }

    this.drawShekhinahSphere = function (x, y) {
        // Note: the sphere is not changed while rotations, and its center is always at the center of symmetry.
        if (this.zShift < 0.0) {
            this.context.lineWidth = this.flags & FLAG_HIGHLIGHT_SPHERE ? 3 : this.lineWidth;
            drawCircle(this.context, x, y, -this.zShift, this.sphereColor);
        }
    }

    this.drawShekhinahSphereCenter = function (x, y, pointRadius) {
        fillCircle(this.context, x, y, pointRadius, this.sphereColor);
    }

    this.rotate = function(xyz) {
        xyz = rotateAroundY(xyz, this.rotationAroundY);
        xyz = rotateAroundZ(xyz, this.rotationAroundZ);
        xyz = rotateAroundX(xyz, this.rotationAroundX);
        return xyz;
    }

    this.height = function() {
        return Math.sqrt(2.0 / 3.0) * this.edgeLength;
    }

    this.setSize = function(size) {
        var intersection = this.day == 0 ? 1.2 : this.day <= 3 ? -0.15 : this.day <= 7 ? 3.0 / 4.0
            : this.day == 8 ? 1.0 : this.day == 9 ? 7.0 / 6.0 : this.day == 10 ? 1.5 : 0;
        this.edgeLength = size;
        this.highlightingLevelWidth = 0.9 * this.edgeLength;
        this.zShift = -0.5 * intersection * this.height();
    }

    this.setDefaultPointColors = function() {
        var baseColor = this.flags & FLAG_LARGE_BASES ? "#a00" : "#f00";
        this.keyPointLowColors = [
            baseColor, baseColor, baseColor,
            "#800", "#800", "#800", "#800", "#800", "#800",
            "#fd9b0e", "#fd9b0e", "#fd9b0e",
            "#9f6109", "#9f6109", "#9f6109",
            "#e8d500", "#e8d500", "#e8d500"
        ];
        this.keyPointHighColors = [
            "#40f", "#40f", "#40f",
            "#208", "#208", "#208", "#208", "#208", "#208",
            "#06f", "#06f", "#06f",
            "#038", "#038", "#038",
            "#0bb", "#0bb", "#0bb"
        ];
        if (this.day == 10 && !(this.flags & FLAG_SKIP_KEY_POINTS_10)) {
            this.keyPointLowColors.push("#40f", "#40f", "#40f");
            this.keyPointHighColors.push("#f00", "#f00", "#f00");
        }
        this.keyPointLowColors.push("#0d0");
        this.keyPointHighColors.push("#0d0");
        if (this.day <= 5) {
            for (var k = 0; k < this.keyPointLowColors.length; k++) {
                if (!this.isVertexColor(k)) {
                    this.keyPointLowColors[k] = "#000";
                    this.keyPointHighColors[k] = "#000";
                }
            }
        }
    }


    this.isVertexColor = function(index) {
        if (this.day == 5) {
            // - in day 5, the vertices of the tetrahedron are still color
            return index < 3 || index == this.keyPointLowColors.length - 1;
        }
        return this.day > 5;
    }

    this.setSpectralHighlightingColors = function() {
        this.highlightingLevelColors = [];
        for (var k = 1; k <= 7; k++) {
            var alpha = this.highlightingLevelsAlpha[k - 1];
            if (alpha == null && (this.flags & FLAG_TRANSPARENT_HIGHLIGHTING)) {
                alpha = DEFAULT_TRANSPARENT_HIGHLIGHTING_ALPHA;
            }
            this.highlightingLevelColors.push(spectralHighlightingColor(k, alpha));
        }
    }

    // private
    this.drawTetrahedronImpl = function (x, y, xyz, isLowTetrahedron, firstInFront) {
        //if (!firstInFront) return; // - for testing
        var invertDash = this.rotationAroundZ > Math.PI;
        // - not too correct: please use angle >180 degree for inverted dash
        var correctedFirstInFront = invertDash ^ firstInFront;
        this.context.lineCap = "round";
        this.context.lineJoin = "round";
        var keyIndex = 0;
        var edgeIndex = 0;
        var keyColors = isLowTetrahedron ? this.keyPointLowColors : this.keyPointHighColors;
        var edgeColors = isLowTetrahedron ? this.keyEdgeLowColors : this.keyEdgeHighColors;
        if (invertDash) {
            this.drawEdge(x + xyz[0].x, y + xyz[0].z, x + xyz[1].x, y + xyz[1].z, firstInFront, edgeColors[edgeIndex++]);
            this.drawEdge(x + xyz[1].x, y + xyz[1].z, x + xyz[2].x, y + xyz[2].z, false, edgeColors[edgeIndex++]);
            this.drawEdge(x + xyz[2].x, y + xyz[2].z, x + xyz[0].x, y + xyz[0].z, firstInFront, edgeColors[edgeIndex++]);
        } else {
            this.drawEdge(x + xyz[0].x, y + xyz[0].z, x + xyz[1].x, y + xyz[1].z, false, edgeColors[edgeIndex++]);
            this.drawEdge(x + xyz[1].x, y + xyz[1].z, x + xyz[2].x, y + xyz[2].z, firstInFront, edgeColors[edgeIndex++]);
            this.drawEdge(x + xyz[2].x, y + xyz[2].z, x + xyz[0].x, y + xyz[0].z, false, edgeColors[edgeIndex++]);
        }
        // - base of tetrahedron
        for (var k = 0; k < 3; k++) {
            if (!(this.flags & FLAG_SKIP_NON_HORIZONTAL_LINES)) {
                this.drawEdge(x + xyz[3].x, y + xyz[3].z, x + xyz[k].x, y + xyz[k].z,
                    k == 0 && !correctedFirstInFront, edgeColors[edgeIndex]);
            }
            edgeIndex++;
            this.addKeyPoint(x , y, xyz[k], keyColors, keyIndex++);
        }
        // - side edges of the tetrahedron and vertices of the base
        for (var k = 0; k < 3; k++) {
            var next = (k + 1) % 3;
            var q1 = between(xyz[k], xyz[next], 2.0 / 3.0);
            var q2 = between(xyz[k], xyz[next], 1.0 / 3.0);
            this.addKeyPoint(x, y, q1, keyColors, keyIndex++);
            this.addKeyPoint(x, y, q2, keyColors, keyIndex++);
        }
        // - key points on the sides of the base
        for (var i = 0; i < this.zHorizontalSections.length; i++) {
            var part = this.zHorizontalSections[i];
            var p = [
                between(xyz[3], xyz[0], part),
                between(xyz[3], xyz[1], part),
                between(xyz[3], xyz[2], part)
            ];
            this.drawEdge(x + p[0].x, y + p[0].z, x + p[1].x, y + p[1].z, !correctedFirstInFront, edgeColors[edgeIndex++]);
            this.drawEdge(x + p[1].x, y + p[1].z, x + p[2].x, y + p[2].z, correctedFirstInFront, edgeColors[edgeIndex++]);
            this.drawEdge(x + p[2].x, y + p[2].z, x + p[0].x, y + p[0].z, !correctedFirstInFront, edgeColors[edgeIndex++]);
            // - all other lines: sides of the intersection triangles
            for (var k = 0; k < 3; k++) {
                this.addKeyPoint(x, y, p[k], keyColors, keyIndex++);
            }
            // - vertices of the intersection triangles
            if (i == 0) {
                if (this.flags & FLAG_ALSO_LITTLE_ADDITIONAL_EDGES) {
                    for (var k = 0; k < 3; k++) {
                        var prev = (k + 2) % 3;
                        var next = (k + 1) % 3;
                        var q1 = between(xyz[k], xyz[next], 2.0 / 3.0);
                        var q2 = between(xyz[k], xyz[next], 1.0 / 3.0);
                        var q = between(p[k], p[next], 0.5);
                        var dash = (!correctedFirstInFront) ^ (k == 1);
                        this.drawEdge(x + p[k].x, y + p[k].z, x + q1.x, y + q1.z, dash, edgeColors[edgeIndex++], true);
                        this.drawEdge(x + q1.x, y + q1.z, x + q.x, y + q.z, dash, edgeColors[edgeIndex++], true);
                        this.drawEdge(x + q2.x, y + q2.z, x + q.x, y + q.z, dash, edgeColors[edgeIndex++], true);
                        this.drawEdge(x + p[next].x, y + p[next].z, x + q2.x, y + q2.z, dash, edgeColors[edgeIndex++], true);
                        // - little "edges" between the base and the 1st intersection triangle
                        var qPrev = between(xyz[k], xyz[prev], 2.0 / 3.0);
                        this.drawEdge(x + qPrev.x, y + qPrev.z, x + q1.x, y + q1.z, firstInFront, edgeColors[edgeIndex++], true);
                    }
                }
                for (var k = 0; k < 3; k++) {
                    var next = (k + 1) % 3;
                    var q = between(p[k], p[next], 0.5);
                    this.addKeyPoint(x, y, q, keyColors, keyIndex++);
                }
                // - middles of the sides of the larger intersection triangle
            }
        }
        this.addKeyPoint(x, y, xyz[3], keyColors, keyIndex++);
        // - the vertex Yetzer / Av ve Lev
        if (this.needHeight) {
            var center = {
                x: (xyz[0].x + xyz[1].x + xyz[2].x) / 3.0,
                y: (xyz[0].y + xyz[1].y + xyz[2].y) / 3.0,
                z: (xyz[0].z + xyz[1].z + xyz[2].z) / 3.0
            };
            this.context.strokeStyle = this.color;
            this.context.lineWidth = this.lineWidth;
            drawLine(this.context, x + xyz[3].x, y + xyz[3].z, x + center.x, y + center.z, true);
        }
        this.context.setLineDash([]);
        // - clearing dash mode
    }

    // private
    this.drawLevelsTitlesImpl = function (x, y, xyzLow, xyzHigh, levelTitle) {
        var yLevels = this.findYLevels(x, y, xyzLow, xyzHigh);
        this.context.strokeStyle = this.levelLineColor;
        this.context.lineCap = "round";
        this.context.lineWidth = this.levelLineWidth;
        this.context.font = this.levelTitleFont;
        var h17 = Math.abs(yLevels[2] - yLevels[1]);
        for (var i = 0; i < yLevels.length; i++) {
            var importantLevel = (this.flags & FLAG_ARROW_ONLY_FOR_1_5_2) == 0 || (i == 0 || i == 3);
            this.context.strokeStyle = this.levelLineColor;
            this.context.fillStyle = importantLevel ? this.levelLineColor : this.levelUnimportantLevelColor;
            var x1 = x;
            var x2 = x + this.levelTitleWidth;
            if (this.day >= 9 && i == 2) {
                drawLine(this.context, x1, yLevels[i], x2, yLevels[i], false);
            }
            if (this.day >= 9 && i >= 2) {
                x1 += this.levelTitleWidth * 1.05;
                x2 += this.levelTitleWidth * 1.05;
            }
            if (this.day >= 9 && i == 3) {
                drawLine(this.context, x1, yLevels[i], x2, yLevels[i], true);
            }
            if (this.day >= 9 && i >= 3) {
                x1 += this.levelTitleWidth * 1.05;
                x2 += this.levelTitleWidth * 1.05;
            }
            drawLine(this.context, x1, yLevels[i], x2, yLevels[i], this.day >= 9 && i == 2);
            if (this.day == 8 && i == 2) {
                x1 += this.levelTitleWidth * 1.05;
                x2 += this.levelTitleWidth * 1.05;
                drawLine(this.context, x1, yLevels[i] - 0.5 * h17, x2, yLevels[i] - 0.5 * h17, true);
                drawLine(this.context, x1, yLevels[i] + 0.5 * h17, x2, yLevels[i] + 0.5 * h17, true);
            }
            if (levelTitle != null) {
                this.context.textBaseline = i == yLevels.length - 1 ? "bottom" : "middle";
                this.context.textAlign = "center";
                var xText = (x1 + x2) / 2;
                var yText = i == yLevels.length - 1 ? yLevels[i] - 10 : 0.5 * (yLevels[i] + yLevels[i + 1]);
                var s = levelTitle == "" ? "" : levelTitle + " ";
                this.context.fillText(s + (i + 2), xText, yText);
            }
            this.context.setLineDash([]);
            // - clearing dash mode
            if (this.day == 8 && i == 2) {
                continue;
            }
            if (this.arrowForLevel) {
                if (importantLevel) {
                    var arrowY1 = yLevels[i];
                    var arrowY2 = (i == yLevels.length - 1 ? yLevels[i] - h17 : yLevels[i + 1]);
                    if (arrowY1 < arrowY2) {
                        arrowY1 += 4;
                        arrowY2 -= 4;
                    } else {
                        arrowY1 -= 4;
                        arrowY2 += 4;
                    }
                    drawArrow(this.context, x2 - 5, arrowY1, x2 - 5, arrowY2, 0.5 * h17, 5, this.levelArrowColor);
                }
            }
        }
        var x1 = x;
        var x2 = x + this.levelTitleWidth;
        if (levelTitle != null) {
            this.context.strokeStyle = this.levelLineColor;
            this.context.fillStyle = this.levelLineColor;
            this.context.textBaseline = "top";
            var s = levelTitle == "" ? "" : levelTitle + " ";
            this.context.fillText(s + 1, (x1 + x2) / 2, yLevels[0] + 10);
        }
        if (this.arrowForLevel) {
            var arrowY = yLevels[0];
            drawArrow(this.context, x2 - 5, arrowY + h17 - 4, x2 - 5, arrowY + 4, 0.5 * h17,
                5, this.levelArrowColor);
        }
    }

    // private
    this.highlightLevelsImpl = function (x, y, xyzLow, xyzHigh) {
        if (this.highlightingLevels.length == 0) {
            return;
        }
        var yLevels = this.findYLevels(x, y, xyzLow, xyzHigh);
        var h17 = (this.day == 10 ? 0.8 : 0.2) * Math.abs(yLevels[yLevels.length - 1] - yLevels[0]);
        var yMiddle = Math.round(0.5 * (yLevels[yLevels.length - 1] + yLevels[0]));
        for (var k = 0; k < this.highlightingLevels.length; k++) {
            var level = this.highlightingLevels[k];
            var halfWidth = 0.5 * this.highlightingLevelWidth;
            if (this.day == 10 && (this.flags & FLAG_REDUCE_HIGHLIGHTING) && (level == 3 || level == 5)) {
                halfWidth -= this.highlightingLevelGapForOverlap;
            }
            if (this.flags & FLAG_EXPAND_HIGHLIGHTING
                || ((this.flags & FLAG_EXPAND_HIGHLIGHTING_4) && level == 4)) {
                halfWidth += this.highlightingLevelExpandingHorizontal;
            }
            var reducedVertical = this.day == 10
                && (this.flags & FLAG_REDUCE_HIGHLIGHTING) && (level == 2 || level == 6);
            var i = level - 1;
            var color = i < this.highlightingLevelColors.length ? this.highlightingLevelColors[i] : "yellow";
            var x1 = x - halfWidth;
            var x2 = x + halfWidth;
            if (this.day == 9 && (this.flags & FLAG_REDUCE_HIGHLIGHTING)) {
                if (level == 3) {
                    x2 = x;
                } else if (level == 5) {
                    x1 = x;
                }
            }
            var y1 = i == 0 ? yLevels[0] + h17 : yLevels[i - 1];
            var y2 = (i == yLevels.length ? yLevels[i - 1] - h17 : yLevels[i]);
            var yA = Math.round(0.75 * y1 + 0.25 * y2);
            var yB = Math.round(0.25 * y1 + 0.75 * y2);
            if (this.day == 8 && (this.flags & FLAG_EXPAND_HIGHLIGHTING) && level == 4) {
                y1 += 0.5 * this.highlightingLevelExpandingVertical;
                y2 -= 0.5 * this.highlightingLevelExpandingVertical;
            }
            if (!(reducedVertical && level == 6)) {
                var altColor = (level == 3 || level == 6) && (this.flags & FLAG_EXPAND_HIGHLIGHTING_4) ?
                    this.highlightingLevelColors[3] :
                    "rgb(255,255,255)";
                var gradient = this.context.createLinearGradient(x, y1, x, yA);
                gradient.addColorStop(0, altColor);
                gradient.addColorStop(1, color);
                this.context.fillStyle = gradient;
                this.context.fillRect(x1, Math.min(y1, yA), x2 - x1, Math.abs(yA - y1));
            }
            this.context.fillStyle = color;
            var yL = yB, yH = yA;
            if (reducedVertical) {
                if (level == 6) {
                    yH = yMiddle;
                } else if (level == 2) {
                    yL = yMiddle;
                }
            }
            this.context.fillRect(x1, Math.min(yL, yH), x2 - x1, Math.abs(yH - yL));
            if (!(reducedVertical && level == 2)) {
                var altColor = (level == 2 || level == 5) && this.flags & FLAG_EXPAND_HIGHLIGHTING_4 ?
                    this.highlightingLevelColors[3] :
                    "rgb(255,255,255)";
                var gradient = this.context.createLinearGradient(x, y2, x, yB);
                gradient.addColorStop(1, color);
                gradient.addColorStop(0, altColor);
                this.context.fillStyle = gradient;
                this.context.fillRect(x1, Math.min(y2, yB), x2 - x1, Math.abs(yB - y2));
            }
        }
    }

    this.drawEdge = function(x1, y1, x2, y2, dash, color, littleAdditional) {
        if (this.lineWidth > 0) {
            this.context.strokeStyle = color != null ? color :
                littleAdditional && this.littleAdditionalLineWidthColor != null ?
                    this.littleAdditionalLineWidthColor :
                    this.color;
            var thickness = color != null ? this.coloredLineWidth :
                littleAdditional ? this.littleAdditionalLineWidth : this.lineWidth ;
            if (thickness > 0) {
                this.context.lineWidth = thickness;
            }
            drawLine(this.context, x1, y1, x2, y2, this.alwaysDash || (this.useDash && dash));
        }
    }

    // private
    this.findYLevels = function(x, y, xyzLow, xyzHigh) {
        // Note: it ignores extra sections of 10th day
        var yLevels = [];
        yLevels.push(y + (xyzLow[0].z + xyzLow[1].z + xyzLow[2].z) / 3.0);
        for (var i = 0; i < Math.min(2, this.zHorizontalSections.length); i++) {
            var part = this.zHorizontalSections[i];
            var p = [
                between(xyzLow[3], xyzLow[0], part),
                between(xyzLow[3], xyzLow[1], part),
                between(xyzLow[3], xyzLow[2], part)
            ];
            yLevels.push(y + (p[0].z + p[1].z + p[2].z) / 3.0);
        }
        var yLevels2 = [];
        yLevels2.push(y + (xyzHigh[0].z + xyzHigh[1].z + xyzHigh[2].z) / 3.0);
        for (var i = 0; i < Math.min(2, this.zHorizontalSections.length); i++) {
            var part = this.zHorizontalSections[i];
            var p = [
                between(xyzHigh[3], xyzHigh[0], part),
                between(xyzHigh[3], xyzHigh[1], part),
                between(xyzHigh[3], xyzHigh[2], part)
            ];
            yLevels2.push(y + (p[0].z + p[1].z + p[2].z) / 3.0);
        }
        for (var i = yLevels2.length - 1; i >= 0; i--) {
            yLevels.push(yLevels2[i]);
        }
        switch (this.day) {
            case 8: {
                if (this.highlightingLevelOnlyLow) {
                    yLevels[3] = yLevels[2];
                } else if (this.highlightingLevelOnlyHigh) {
                    yLevels[2] = yLevels[3];
                } else {
                    yLevels[2] = yLevels[3] = Math.round(0.5 * (yLevels[2] + yLevels[3]));
                }
                break;
            }
            case 9: {
                yLevels[1] = yLevels[3] = Math.round(0.5 * (yLevels[1] + yLevels[3]));
                yLevels[2] = yLevels[4] = Math.round(0.5 * (yLevels[2] + yLevels[4]));
                break;
            }
            case 10: {
                yLevels[0] = yLevels[3] = Math.round(0.5 * (yLevels[0] + yLevels[3]));
                yLevels[2] = yLevels[5] = Math.round(0.5 * (yLevels[2] + yLevels[5]));
                break;
            }
        }
        return yLevels;
    }

    // private
    this.addKeyPoint = function(centerX, centerY, p, keyColors, index) {
        var color = keyColors[index];
        if (this.keyPointInsideAtOutsideColors != null && this.keyPointInsideAtOutsideColors.length >= 3) {
            var distance = Math.sqrt(p.x * p.x + p.y * p.y + p.z * p.z);
            var shekhinahRadius = -this.zShift;
            // - will be negative if there is no Shekhinah sphere (tetrahedrons do not overlap)
            if (Math.abs(distance - shekhinahRadius) < 0.00001) {
                color = this.keyPointInsideAtOutsideColors[1];
            } else if (distance < shekhinahRadius) {
                color = this.keyPointInsideAtOutsideColors[0];
            } else {
                color = this.keyPointInsideAtOutsideColors[2];
            }
        }
        if (color == null) {
            return;
        }
        if (this.differentBases && index >= 3) {
            if (this.differentBasesOnlyFirst3Points) {
                return;
            } else {
                color = "#888";
            }
        }
        if (this.differentBases && index == (this.symmetry ? 0 : 2)) {
            color = "#000";
        }
        if (this.differentBases && index == 1) {
            color = "yellow";
        }
        if (this.differentBases && index == (this.symmetry ? 2 : 0)) {
            color = "#fff";
        }
        var contourColor = this.differentBases ? "#000" : null;
        var r = this.keyPointRadius;
        if ((this.flags & FLAG_LITTLE_BLACK_KEY_POINTS) && !this.isVertexColor(index)) {
            r = this.blackKeyPointRadius;
        }
        if (this.flags & FLAG_WEAK_KEY_POINTS) {
            color = "#888";
            contourColor = null;
        }
        fillCircle(this.context, centerX + p.x, centerY + p.z, r, color, contourColor);
    }
}

function EggWithTetrahedrons(context, width, height) {
    this.setSizes = function(width, height) {
        this.width = width == null ? 0.7 * height : width;
        this.height = height;
        return this;
    }

    this.setLineWidth = function(lineWidth) {
        this.lineWidth = lineWidth;
        return this;
    }

    this.context = context;
    this.setSizes(width, height);
    this.yolkDiameter = null; // - calculated automatically
    this.lineWidth = 3;
    this.yolkLineWidth = 1;
    this.needCenter = true;
    this.dashed = false;
    this.color = "#FFFFFF";
    this.yolkColor = "#FFFF00";
    this.borderColor = "#000000";
    this.yolkBorderColor = "#000000";
    this.centerColor = "#FF0000";

    this.yolkCenterYShift = 0.15;
    this.yolkRelativeSize = 0.6;
    this.centerRelativeSize = 0.1;
    this.tetrahedronsRelativeSize = 0.7;

    this.kappa = 0.5;
    this.middleYPosition = 0.6;

    this.setSaturatedColors = function() {
        this.color = "#FFFFFF"; // keep same colors - it was bad idea
        this.yolkColor  = "#FFFF00";
        return this;
    }

    this.defaultTetrahedons = function() {
        return makeTwoTetrahedrons(
            context,
            context.canvas,
            10,
            FLAG_THIN | FLAG_SKIP_SPHERE | FLAG_SKIP_KEY_POINTS,
            null,
            this.tetrahedronsRelativeSize * this.getYolkDiameter());
    }

    this.draw = function(x, y) {
        this.drawOnlyEgg(x, y);
        this.drawYolk(x, y);
        if (this.needCenter) {
            this.drawCenter(x, y);
        }
        if (this.tetrahedrons != null) {
            this.tetrahedrons.drawFullConfiguration(x, this.yolkY(y));
        }
    }

    this.drawOnlyEgg = function(x, y) {
        var w = this.width;
        var h = this.height;
        x -= w / 2;
        y -= h / 2;
        var ox = (w / 2) * this.kappa,         // control point offset horizontal
            oy = (h / 2) * this.kappa,         // control point offset vertical
            xe = x + w,                        // x-end
            ye = y + h,                        // y-end
            xm = x + w / 2,                    // x-middle
            ym = y + this.middleYPosition * h; // y-middle
        this.context.beginPath();
        this.context.moveTo(x, ym);
        this.context.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);
        this.context.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);
        this.context.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);
        this.context.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym);
        // this.context.closePath(); // - not necessary, path is already closed
        if (this.color != null) {
            this.context.fillStyle = "#FFFFFF"; // for a case if the following assignment will not work (MSIE)
            this.context.fillStyle = this.color;
            this.context.fill();
        }
        if (this.borderColor != null) {
            this.context.lineWidth = this.lineWidth;
            this.context.strokeStyle = this.borderColor;
            this.context.setLineDash(this.dashed ? [5, 10] : []);
            this.context.stroke();
        }
    }

    this.drawYolk = function(x, y) {
        var r = 0.5 * this.getYolkDiameter();
        y = this.yolkY(y);
        this.context.lineWidth = this.yolkLineWidth;
        this.context.setLineDash(this.dashed ? [5, 10] : []);
        this.context.fillStyle = "#FFFF00"; // for a case if the following assignment will not work (MSIE)
        this.context.fillStyle = this.yolkColor;
        this.context.beginPath();
        this.context.arc(x, y, r, 0, 2 * Math.PI, false);
        this.context.closePath();
        this.context.fill();
        this.context.strokeStyle = this.yolkBorderColor;
        this.context.stroke();
    }

    this.drawCenter = function(x, y) {
        var r = 0.5 * this.centerRelativeSize * this.width;
        y = this.yolkY(y);
        fillCircle(this.context, x, y, r, this.centerColor, null);
    }


    this.getYolkDiameter = function() {
        return this.yolkDiameter != null ? this.yolkDiameter : this.yolkRelativeSize * this.width;
    }

    //private
    this.yolkY = function(y) {
        return y + this.yolkCenterYShift * this.height;
    }

    this.tetrahedrons = this.defaultTetrahedons();
}

function spectralHighlightingColor(level, alpha) {
    return alpha == null ?
        "hsl(" + LEVELS_H[level - 1] + ",100%,50%)" :
        "hsla(" + LEVELS_H[level - 1] + ",100%,50%," + alpha + ")";
        // - important: MSIE works only with the form like "hsla(300,100%,50%,0.2)";
        // for example, it does not support "rgba(...)"
}

var SINGLE_TRIANGLE = 1;
var BOTH_TRIANGLES = 2;
var MAIN_AND_SECONDARY_TRIANGLES = 3;
var TRIANGLE_AND_ROTATED = 4;
var FLAG_ARROW = 8;
var FLAG_ARROW_ANTICLOCKWISE = 16;
var TRIANGLE_MODE_MASK = 7;
var TRIANGLE_FLAG_MASK = 31;
var TRIANGLE_POINT_INDEXES = [0, 1, 2, 9];
function rotatingTriangle(context, canvas, scale, mode, rotation, basesColor) {
    if (scale == null) {
        scale = 1.0;
    }
    if (mode == null) {
        mode = TRIANGLE_AND_ROTATED;
    }
    var hasRotation = rotation != null;
    rotation = rotation == null ? 0 : rotation;
    var tetrahedrons = new Tetrahedrons(context);
    tetrahedrons.lineWidth = (mode & FLAG_SKIP_LINES) != 0 ? 0 : (mode & FLAG_THIN) != 0 ? 1 : 2;
    tetrahedrons.alwaysDash = (mode & FLAG_ALWAYS_DASH) != 0;
    var littleKeyPoints = (mode & FLAG_LITTLE_KEY_POINTS) != 0;
    tetrahedrons.symmetry = (mode & FLAG_SYMMETRY) != 0;
    tetrahedrons.useDash = false;
    tetrahedrons.differentBases = (mode & FLAG_DIFFERENT_BASES) != 0;
    tetrahedrons.flags = mode & ~TRIANGLE_FLAG_MASK;
    var needArrow = (mode & FLAG_ARROW) != 0 || (mode & FLAG_ARROW_ANTICLOCKWISE) != 0;
    var anticlockwiseArrow = (mode & FLAG_ARROW_ANTICLOCKWISE) != 0;
    var largeBasesFlag = (mode & FLAG_LARGE_BASES) != 0;
    mode &= TRIANGLE_MODE_MASK;
    tetrahedrons.edgeLength = 0.80 * scale * Math.min(canvas.width, canvas.height);
    for (var k = 0; k < TRIANGLE_POINT_INDEXES.length; k++) {
        var index = TRIANGLE_POINT_INDEXES[k];
        tetrahedrons.keyPointHighColors[index] = tetrahedrons.keyPointLowColors[index] =
             index <= 3 && basesColor != null ? basesColor : "#000";
    }

    // - without zHorizontalSections, we have 9 keypoints at the base triangle
    tetrahedrons.keyPointRadius = 3;
    tetrahedrons.rotationAroundX = Math.PI * 90.0 / 180.0;
    if (mode == TRIANGLE_AND_ROTATED) {
        tetrahedrons.alwaysDash = true;
        tetrahedrons.rotationAroundZ = Math.PI * (rotation + 210.0) / 180.0;
        tetrahedrons.drawLowTetrahedron(canvas.width / 2, canvas.height / 2);
        // - little rotated
        tetrahedrons.alwaysDash = (mode & FLAG_ALWAYS_DASH) != 0;
    }
    if (mode == BOTH_TRIANGLES || mode == MAIN_AND_SECONDARY_TRIANGLES) {
        tetrahedrons.rotationAroundZ = Math.PI * (rotation + 180.0) / 180.0;
        tetrahedrons.useDash = false;
        var save = tetrahedrons.color;
        if (mode == MAIN_AND_SECONDARY_TRIANGLES) {
            tetrahedrons.color = "#bbb";
        }
        for (var k = 0; k < TRIANGLE_POINT_INDEXES.length; k++) {
            var index = TRIANGLE_POINT_INDEXES[k];
            tetrahedrons.keyPointHighColors[index]= tetrahedrons.color;
        }
        tetrahedrons.drawHighTetrahedron(canvas.width / 2, canvas.height / 2);
        tetrahedrons.color = save;
    }
    tetrahedrons.keyPointRadius = littleKeyPoints ? 3 : largeBasesFlag ? 7 : 5;
    tetrahedrons.rotationAroundZ = Math.PI * (rotation + 180.0) / 180.0;
    tetrahedrons.keyPointLowColors[9] = null;
    // - use small point from high tetrahedron
    tetrahedrons.drawLowTetrahedron(canvas.width / 2, canvas.height / 2);
    if (needArrow) {
        var x1 = canvas.width / 2 + 7 * 0.01 * scale * canvas.width * (anticlockwiseArrow ? -1 : 1);
        var y1 = canvas.height / 2 - 48 * 0.01 * scale * canvas.height;
        var x2 = canvas.width / 2 + 20 * 0.01 * scale * canvas.width * (anticlockwiseArrow ? -1 : 1);
        var y2 = canvas.height / 2 - 42 * 0.01 * scale * canvas.height;
        // - these coordinates are chosen manually
        drawArrow(context, x1, y1, x2, y2, 9, 3, "#F00");
    }
}

var QUICK_MOVEMENT_120 = [0, 0, 0, 0, 0, 3, 10, 40, 60, 80, 110, 117, 120, 120, 120, 120, 120];

function initializeRotation(object) {
    if (object.rotationIndex == null) {
        object.rotationIndex = 0;
        object.phase = 0;
        object.stablePosition = true;
        object.rotationFlags = 0;
        object.rotation = 0;
    }
}

function incrementRotation120(object, skipLinesWhenNonStable, positions) {
    incrementOrDecrementRotation120(object, skipLinesWhenNonStable, positions, true);
}

function decrementRotation120(object, skipLinesWhenNonStable, positions) {
    incrementOrDecrementRotation120(object, skipLinesWhenNonStable, positions, false);
}

function incrementOrDecrementRotation120(object, skipLinesWhenNonStable, positions, doIncrement) {
    initializeRotation(object);
    if (positions == null) {
        positions = QUICK_MOVEMENT_120;
    }
    var m = 3 * positions.length;
    object.rotationIndex = (object.rotationIndex + (doIncrement ? 1 : m - 1)) % m;
    var index = object.rotationIndex % positions.length;
    object.phase = Math.round(object.rotationIndex / positions.length) % 3;
    var phase = Math.floor(object.rotationIndex / positions.length);
    object.stablePosition = positions[index] % 120 == 0;
    object.rotationFlags = object.stablePosition ? 0 :
        (skipLinesWhenNonStable ? FLAG_SKIP_LINES : FLAG_THIN)
        | FLAG_ALWAYS_DASH | FLAG_LITTLE_KEY_POINTS | FLAG_WEAK_KEY_POINTS;
    object.rotation = phase * 120 + positions[index];
}

// private
function tetrahedronVertexes(edgeLength, zShift, highTetrahedron) {
    var base = [
        {x: 0.0, y: 2.0 * Math.sqrt(3.0) / 6.0},
        {x: -0.5, y: -Math.sqrt(3.0) / 6.0},
        {x: 0.5, y: -Math.sqrt(3.0) / 6.0}
    ];
    if (highTetrahedron) {
        base[1].x = -base[1].x;
        base[2].x = -base[2].x;

    }
    var height = Math.sqrt(2.0 / 3.0) * edgeLength;
    var result = [];
    for (var k = 0; k < base.length; k++) {
        result.push({x: base[k].x * edgeLength, y: base[k].y * edgeLength, z: zShift + height});
    }
    result.push({x: 0.0, y: 0.0, z: zShift});
    if (highTetrahedron) {
        result = rotateAroundXCosSin(result, -1, 0);
    }
    return result;
}


