(function() {

// === PAINT OBJECTS ===

CanvasLineSegment = Utilities.createClass(
    function(stage)
    {
        var circle = Stage.randomInt(0, 3);
        this._color = ["#e01040", "#10c030", "#744CBA", "#e05010"][circle];
        this._lineWidth = Math.pow(Pseudo.random(), 12) * 20 + 3;
        this._omega = Pseudo.random() * 3 + 0.2;
        var theta = Stage.randomAngle();
        this._cosTheta = Math.cos(theta);
        this._sinTheta = Math.sin(theta);
        this._startX = stage.circleRadius * this._cosTheta + stage.circleX[circle];
        this._startY = stage.circleRadius * this._sinTheta + stage.circleY[circle];
        this._length = Math.pow(Pseudo.random(), 8) * stage.lineLengthMaximum + stage.lineMinimum;
        this._segmentDirection = Pseudo.random() > 0.5 ? -1 : 1;
    }, {

    draw: function(context)
    {
        context.strokeStyle = this._color;
        context.lineWidth = this._lineWidth;

        this._length += Math.sin(Stage.dateCounterValue(100) * this._omega);

        context.beginPath();
        context.moveTo(this._startX, this._startY);
        context.lineTo(this._startX + this._segmentDirection * this._length * this._cosTheta,
                       this._startY + this._segmentDirection * this._length * this._sinTheta);
        context.stroke();
    }
});

CanvasArc = Utilities.createClass(
    function(stage)
    {
        var maxX = 6, maxY = 3;
        var distanceX = stage.size.x / maxX;
        var distanceY = stage.size.y / (maxY + 1);
        var randY = Stage.randomInt(0, maxY);
        var randX = Stage.randomInt(0, maxX - 1 * (randY % 2));

        this._point = new Point(distanceX * (randX + (randY % 2) / 2), distanceY * (randY + .5));

        this._radius = 20 + Math.pow(Pseudo.random(), 5) * (Math.min(distanceX, distanceY) / 1.8);
        this._startAngle = Stage.randomAngle();
        this._endAngle = Stage.randomAngle();
        this._omega = (Pseudo.random() - 0.5) * 0.3;
        this._counterclockwise = Stage.randomBool();
        var colors = ["#101010", "#808080", "#c0c0c0"];
        colors.push(["#e01040", "#10c030", "#e05010"][(randX + Math.ceil(randY / 2)) % 3]);
        this._color = colors[Math.floor(Pseudo.random() * colors.length)];
        this._lineWidth = 1 + Math.pow(Pseudo.random(), 5) * 30;
        this._doStroke = Stage.randomInt(0, 3) != 0;
    }, {

    draw: function(context)
    {
        this._startAngle += this._omega;
        this._endAngle += this._omega / 2;

        if (this._doStroke) {
            context.strokeStyle = this._color;
            context.lineWidth = this._lineWidth;
            context.beginPath();
            context.arc(this._point.x, this._point.y, this._radius, this._startAngle, this._endAngle, this._counterclockwise);
            context.stroke();
        } else {
            context.fillStyle = this._color;
            context.beginPath();
            context.lineTo(this._point.x, this._point.y);
            context.arc(this._point.x, this._point.y, this._radius, this._startAngle, this._endAngle, this._counterclockwise);
            context.lineTo(this._point.x, this._point.y);
            context.fill();
        }
    }
});

// CanvasLinePoint contains no draw() method since it is either moveTo or
// lineTo depending on its index.
CanvasLinePoint = Utilities.createClass(
    function(stage)
    {
        var colors = ["#101010", "#808080", "#c0c0c0", "#101010", "#808080", "#c0c0c0", "#e01040"];
        this.color = Stage.randomElementInArray(colors);
        this.width = Math.pow(Pseudo.random(), 5) * 20 + 1;
        this.isSplit = Pseudo.random() > 0.95;

        var nextPoint;
        if (stage.objects.length)
            nextPoint = this.randomPoint(stage, stage.objects[stage.objects.length - 1].coordinate);
        else
            nextPoint = this.randomPoint(stage, this.gridSize.center);
        this.point = nextPoint.point;
        this.coordinate = nextPoint.coordinate;
    }, {

    gridSize: new Point(80, 40),
    offsets: [
        new Point(-4, 0),
        new Point(2, 0),
        new Point(1, -2),
        new Point(1, 2),
    ],

    randomPoint: function(stage, startCoordinate)
    {
        var coordinate = startCoordinate;
        if (stage.objects.length) {
            var offset = Stage.randomElementInArray(this.offsets);

            coordinate = coordinate.add(offset);
            if (coordinate.x < 0 || coordinate.x > this.gridSize.width)
                coordinate.x -= offset.x * 2;
            if (coordinate.y < 0 || coordinate.y > this.gridSize.height)
                coordinate.y -= offset.y * 2;
        }

        var x = (coordinate.x + .5) * stage.size.x / (this.gridSize.width + 1);
        var y = (coordinate.y + .5) * stage.size.y / (this.gridSize.height + 1);
        return {
            point: new Point(x, y),
            coordinate: coordinate
        };
    },

    draw: function(context)
    {
        context.lineTo(this.point.x, this.point.y);
    }
});

CanvasQuadraticSegment = Utilities.createSubclass(CanvasLinePoint,
    function(stage)
    {
        CanvasLinePoint.call(this, stage);
        // The chosen point is instead the control point.
        this._point2 = this.point;

        // Get another random point for the actual end point of the segment.
        var nextPoint = this.randomPoint(stage, this.coordinate);
        this.point = nextPoint.point;
        this.coordinate = nextPoint.coordinate;
    }, {

    draw: function(context)
    {
        context.quadraticCurveTo(this._point2.x, this._point2.y, this.point.x, this.point.y);
    }
});

CanvasBezierSegment = Utilities.createSubclass(CanvasLinePoint,
    function(stage)
    {
        CanvasLinePoint.call(this, stage);
        // The chosen point is instead the first control point.
        this._point2 = this.point;
        var nextPoint = this.randomPoint(stage, this.coordinate);
        this._point3 = nextPoint.point;

        nextPoint = this.randomPoint(stage, nextPoint.coordinate);
        this.point = nextPoint.point;
        this.coordinate = nextPoint.coordinate;
    }, {

    draw: function(context, off)
    {
        context.bezierCurveTo(this._point2.x, this._point2.y, this._point3.x, this._point3.y, this.point.x, this.point.y);
    }
});

// === STAGES ===

CanvasLineSegmentStage = Utilities.createSubclass(SimpleCanvasStage,
    function()
    {
        SimpleCanvasStage.call(this, CanvasLineSegment);
    }, {

    initialize: function(benchmark, options)
    {
        SimpleCanvasStage.prototype.initialize.call(this, benchmark, options);
        this.context.lineCap = options["lineCap"] || "butt";
        this.lineMinimum = 20;
        this.lineLengthMaximum = 40;
        this.circleRadius = this.size.x / 8 - .4 * (this.lineMinimum + this.lineLengthMaximum);
        this.circleX = [
            5.5 / 32 * this.size.x,
            12.5 / 32 * this.size.x,
            19.5 / 32 * this.size.x,
            26.5 / 32 * this.size.x,
        ];
        this.circleY = [
            2.1 / 3 * this.size.y,
            0.9 / 3 * this.size.y,
            2.1 / 3 * this.size.y,
            0.9 / 3 * this.size.y
        ];
        this.halfSize = this.size.multiply(.5);
        this.twoFifthsSizeX = this.size.x * .4;
    },

    animate: function()
    {
        var context = this.context;
        context.clearRect(0, 0, this.size.x, this.size.y);

        var angle = Stage.dateFractionalValue(3000) * Math.PI * 2;
        var dx = this.twoFifthsSizeX * Math.cos(angle);
        var dy = this.twoFifthsSizeX * Math.sin(angle);

        var gradient = context.createLinearGradient(this.halfSize.x + dx, this.halfSize.y + dy, this.halfSize.x - dx, this.halfSize.y - dy);
        var gradientStep = 0.5 + 0.5 * Math.sin(Stage.dateFractionalValue(5000) * Math.PI * 2);
        var colorStopStep = Utilities.lerp(gradientStep, -.1, .1);
        var brightnessStep = Math.round(Utilities.lerp(gradientStep, 32, 64));
        var color1Step = "rgba(" + brightnessStep + "," + brightnessStep + "," + (brightnessStep << 1) + ",.4)";
        var color2Step = "rgba(" + (brightnessStep << 1) + "," + (brightnessStep << 1) + "," + brightnessStep + ",.4)";
        gradient.addColorStop(0, color1Step);
        gradient.addColorStop(.2 + colorStopStep, color1Step);
        gradient.addColorStop(.8 - colorStopStep, color2Step);
        gradient.addColorStop(1, color2Step);

        context.lineWidth = 15;
        for(var i = 0; i < 4; i++) {
            context.strokeStyle = ["#e01040", "#10c030", "#744CBA", "#e05010"][i];
            context.fillStyle = ["#70051d", "#016112", "#2F0C6E", "#702701"][i];
            context.beginPath();
                context.arc(this.circleX[i], this.circleY[i], this.circleRadius, 0, Math.PI*2);
                context.stroke();
                context.fill();
            context.fillStyle = gradient;
                context.fill();
        }

        for (var i = this.offsetIndex, length = this.objects.length; i < length; ++i)
            this.objects[i].draw(context);
    }
});

CanvasLinePathStage = Utilities.createSubclass(SimpleCanvasStage,
    function()
    {
        SimpleCanvasStage.call(this, [CanvasLinePoint, CanvasLinePoint, CanvasQuadraticSegment, CanvasBezierSegment]);
    }, {

    initialize: function(benchmark, options)
    {
        SimpleCanvasStage.prototype.initialize.call(this, benchmark, options);
        this.context.lineJoin = options["lineJoin"] || "bevel";
        this.context.lineCap = options["lineCap"] || "butt";
    },

    animate: function() {
        var context = this.context;

        context.clearRect(0, 0, this.size.x, this.size.y);
        context.beginPath();
        for (var i = this.offsetIndex, length = this.objects.length; i < length; ++i) {
            var object = this.objects[i];
            if (i == this.offsetIndex) {
                context.lineWidth = object.width;
                context.strokeStyle = object.color;
                context.beginPath();
                context.moveTo(object.point.x, object.point.y);
            } else {
                object.draw(context);

                if (object.isSplit) {
                    context.stroke();

                    context.lineWidth = object.width;
                    context.strokeStyle = object.color;
                    context.beginPath();
                    context.moveTo(object.point.x, object.point.y);
                }

                if (Pseudo.random() > 0.995)
                    object.isSplit = !object.isSplit;
            }
        }
        context.stroke();
    }
});

// === BENCHMARK ===

CanvasPathBenchmark = Utilities.createSubclass(Benchmark,
    function(options)
    {
        var stage;
        switch (options["pathType"]) {
        case "line":
            stage = new CanvasLineSegmentStage();
            break;
        case "linePath":
            stage = new CanvasLinePathStage();
            break;
        case "arcs":
            stage = new SimpleCanvasStage(CanvasArc);
            break;
        }

        Benchmark.call(this, stage, options);
    }
);

window.benchmarkClass = CanvasPathBenchmark;

})();