/**
 * @author Patrick Kopp
 */
// +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ CIRCLE ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
StrategyCircle.prototype = new CircleBasedStrategy;
StrategyCircle.prototype.STRATEGYNAME = "Circle - Strategy";

function StrategyCircle(steps, center, radius, direction) {

    this.base = CircleBasedStrategy;
    this.base(steps, center, radius, direction);
    
    this.moveItems = function() {
    
        var baseCoords = this.getBaseCoords();
        
        for (var i = 0; i < this.items.length; i++) {
            this.items[i].setPosition(baseCoords[i].add(this.center));
        }
    }
    
    this.getPositions = function(amount) {
    
        var baseCoords = this.getBaseCoords(amount);
        var ret = [];
        
        for (var i = 0; i < amount; i++) {
            ret[i] = baseCoords[i].add(this.center);
        }
        
        return ret;
    }
    
    this.perform = function() {
        this.moveItems();
        this._nextFrame();
    }
    
}

// +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ TWISTER ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
StrategyTwister.prototype = new CircleBasedStrategy;
StrategyTwister.prototype.STRATEGYNAME = "Twister Strategy";

function StrategyTwister(steps, center, radius, angularSpeed, direction) {
    
    this.base = CircleBasedStrategy;
    this.base(steps, center, radius, direction);
    
    this.angularSpeed = angularSpeed;
    var alpha = 0;
        
    this.moveItems = function() {
    
        var baseCoords = this.getBaseCoords();
        var tmp = Math.cos(alpha);
        
        for (var i = 0; i < this.items.length; i++) {

            var baseCoord = baseCoords[i];
            var x = baseCoord.x * tmp + this.center.x;
            var y = baseCoord.y * tmp + this.center.y;
            
            this.items[i].setPosition(new Point(x, y));
        }
        
        alpha = this.getIncrementedAlpha(alpha, this.angularSpeed, alpha);
    }
    
    this.perform = function() {
        this.moveItems();
        this._nextFrame();
    }
    
}

// +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ TWISTER ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ellipse.prototype = new CircleBasedStrategy;
Ellipse.prototype.STRATEGYNAME = "Ellipse";

function Ellipse(steps, 
                 center, 
                 radius, 
                 direction,
                 secondarySpeed, 
                 secondaryRadius,
                 mainRadiusIncr,
                 ellipseRotationIncr, 
                 radiusMin, 
                 radiusMax) {
    
    this.base = CircleBasedStrategy;
    this.base(steps, center, radius, direction);
    this.secondaryFrameOffset = 0;
    this.theta = 0;
    this.anotherRotation = EllipseRotationDirection.valueOf(this.direction).revert();
    
    this.secondarySpeed = secondarySpeed;
    this.secondaryRadius = secondaryRadius;
    this.mainRadiusIncr = mainRadiusIncr;
    this.ellipseRotationIncr = ellipseRotationIncr;
    this.radiusMax = radiusMax;
    this.radiusMin = radiusMin;
    
    this.secondaryOffsetDeg = CircleBasedStrategy.TWO_PI / this.secondarySpeed;
    
    this.moveItems = function() {
    
        var baseCoords = this.getBaseCoords();
        
        for (var i = 0; i < this.items.length; i++) {

            var baseCoord = baseCoords[i];
            var x = baseCoord.x + this.center.x;
            var y = baseCoord.y + this.center.y;
            
            this.items[i].setPosition(new Point(x, y));
        }
        
        this.secondaryRadius += this.ellipseRotationIncr;
        
        if(this.secondaryRadius > this.radiusMax || this.secondaryRadius < this.radiusMin) {
            this.ellipseRotationIncr *= -1;
        }
        
        this.radius += this.mainRadiusIncr;
        
        if(this.radius > this.radiusMax || this.radius < this.radiusMin) {
            this.mainRadiusIncr *= -1;
        }
        
    }
    
    this.getBaseCoords = function(amount) {
    
        var pos = new Array();
        var itemCount = amount || this.getItemsCount();
        var offsetDeg = this.getOffsetInDegrees();
        var frameNo = this.getFrame();
        var xTheta = this.anotherRotation.getX(this.theta);
        var yTheta = this.anotherRotation.getY(this.theta);
        
        for (var i = 1; i <= itemCount; i++) {
        
            // Position on the circle in radians
            var fPi = (i * this.frameOffset + frameNo) * offsetDeg;
            
            //var x = this.direction.getX(this.radius, fPi) * this.anotherRotation.getX(this.theta) 
            // - this.direction.getY(this.secondaryRadius, fPi) * this.anotherRotation.getY(this.theta);
             
            //var y = this.direction.getY(this.radius, fPi) * this.anotherRotation.getY(this.theta) 
            // + this.direction.getX(this.secondaryRadius, fPi) * this.anotherRotation.getX(this.theta);
            
            //var x = this.direction.getX(this.radius, fPi);
            //var y = this.direction.getY(this.secondaryRadius, fPi);
            
            var xx = this.direction.getX(this.radius, fPi);
            var yy = this.direction.getY(this.secondaryRadius, fPi);
            
            var x = xx * xTheta - yy * yTheta;
            var y = xx * yTheta + yy * xTheta;
                        
            pos.push(new Point(x, y));;
            
        }
        
        this.theta += (1 / this.secondarySpeed);
        
        if (this.theta >= CircleBasedStrategy.TWO_PI) { // one amplitude is done, reset
            this.theta = 0; // to avoid overflows
        }
        
        //window.status = this.theta;
        
        return pos;
    }
    
    this.updateFrameOffset = function() {
    
        var itemCount = this.getItemsCount();
        
        if (itemCount > 0) {
            this.frameOffset = this.steps / itemCount;
            this.secondaryFrameOffset = this.secondarySpeed / itemCount;
        }
    }
    
    this.revertRotationDirection = function() {
    
        this.direction = this.direction.revert();
        this.anotherRotation = this.anotherRotation.revert();
        this.smoothPositionTransition();
    }
    
    this.perform = function() {
        this.moveItems();
        this._nextFrame();
    }
    
}

var EllipseRotationDirection = {
  
    "valueOf" : function (directon) {
        
        return directon == RotationDirection.CW ? EllipseRotationDirection.CW : EllipseRotationDirection.CCW;
    },
  
    "CW" : {
        "getX" : function (theta) {

            return Math.cos(theta);
        },
        
        "getY" : function (theta) {
            
            return Math.sin(theta);
        },
        
        "revert" : function () {
            
            return EllipseRotationDirection.CCW;
        },      
        
        "toString" : function() {
            
            return "clockwise";
        }
    },
   
    "CCW" : {
        "getX" : function (theta) {

            return Math.sin(theta);
        },
        
        "getY" : function (theta) {
            
            return Math.cos(theta);
        },
        
        "revert" : function () {
            
            return EllipseRotationDirection.CW;
        },     
        
        "toString" : function() {
            
            return "counterclockwise";
        }     
    }
};

// +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ROTATION-X ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
StrategyRotationX.prototype = new CircleBasedStrategy;
StrategyRotationX.prototype.STRATEGYNAME = "Rotation - X Strategy";

function StrategyRotationX(steps, center, radius, angularSpeed) {

    this.base = CircleBasedStrategy;
    this.base(steps, center, radius);
    
    var angularSpeed = angularSpeed;
    var alpha = 0;
    
    this.moveItems = function() {
        
        var baseCoords = this.getBaseCoords();
        var tmp = alpha * angularSpeed;
        
        for (var i = 0; i < this.items.length; i++) {

            var baseCoord = baseCoords[i];
            var x = baseCoord.x * Math.cos(tmp) + this.center.x;
            var y = baseCoord.y + this.center.y;
            
            this.items[i].setPosition(new Point(x, y));
        }
        
        alpha = this.getIncrementedAlpha(alpha, angularSpeed, tmp);
    }
    
    this.perform = function() {
        this.moveItems();
        this._nextFrame();
    }
}

// +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ROTATION-Y ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
StrategyRotationY.prototype = new CircleBasedStrategy;
StrategyRotationY.prototype.STRATEGYNAME = "Rotation - Y Strategy";

function StrategyRotationY(steps, center, radius, angularSpeed) {

    this.base = CircleBasedStrategy;
    this.base(steps, center, radius);
    
    var angularSpeed = angularSpeed;
    var alpha = 0;
    
    this.moveItems = function() {

        var baseCoords = this.getBaseCoords();
        var tmp = alpha * angularSpeed;

        for (var i = 0; i < this.items.length; i++) {

            var baseCoord = baseCoords[i];
            var x = baseCoord.x + this.center.x;
            var y = baseCoord.y * Math.cos(tmp) + this.center.y;
            
            this.items[i].setPosition(new Point(x, y));
        }

        alpha = this.getIncrementedAlpha(alpha, angularSpeed, tmp);
    }
    
    this.perform = function() {
        this.moveItems();
        this._nextFrame();
    }
    
}

// +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ HORIZONTAL-HOP ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
StrategyHorizontalHop.prototype = new CircleBasedStrategy;
StrategyHorizontalHop.prototype.STRATEGYNAME = "Horizontal Hop Strategy";

function StrategyHorizontalHop(steps, center, radius, angularSpeed) {

    this.base = CircleBasedStrategy;
    this.base(steps, center, radius);
    
    var angularSpeed = angularSpeed;
    var alpha = 0;
    
    var direction = 1; // sliding direction: 1-left&right, 2-one side
    var isLeft = false;
    
    /**
     * if oneSide is true it will hop just to one side.
     * @param {Boolean} isOneSide
     */
    this.setHopToBothSides = function(isOneSide) {
        if (isOneSide) {
            direction = 2;
        }
        else {
            direction = 1;
        }
    }
    
    /**
     * if true it will hop only to the left otherwise to the right.
     * @param {Boolean} isLeft
     */
    this.setHoppingSideLeft = function(sideIsLeft) {
        isLeft = sideIsLeft;
    }
    
    this.moveItems = function() {
        
        var baseCoords = this.getBaseCoords();
        
        var alphaTimesAng = alpha * angularSpeed;
        
        for (var i = 0; i < this.items.length; i++) {
          
            var baseCoord = baseCoords[i];
            var tmp = ((this.center.x + baseCoord.x - this.radius) *  Math.pow(Math.cos(alphaTimesAng), direction));
            var x = this.center.x;
            if (isLeft && this.direction != 1) {
                x -= tmp;
            }
            else {
                x += tmp;
            }
            var y = baseCoord.y + this.center.y;
            this.items[i].setPosition(new Point(x, y));
        }
        
        alpha = this.getIncrementedAlpha(alpha, angularSpeed, alphaTimesAng);
    }
    
    this.perform = function() {
        this.moveItems();
        this._nextFrame();
    }
    
}

// +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ VERTICAL-HOP ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
StrategyVerticalHop.prototype = new CircleBasedStrategy;
StrategyVerticalHop.prototype.STRATEGYNAME = "Vertical Hop Strategy";

function StrategyVerticalHop(steps, center, radius, angularSpeed) {

    this.base = CircleBasedStrategy;
    this.base(steps, center, radius);
    
    var angularSpeed = angularSpeed;
    var alpha = 0;
    
    var direction = 1;
    var isUp = false;
    
    this.setHopToBothSides = function(isOneSide) {
        if (isOneSide) {
            direction = 2;
        }
        else {
            direction = 1;
        }
    }
    
    this.setHoppingSideUp = function(sideIsUp) {
        isUp = sideIsUp;
    }
    
    this.moveItems = function() {

        var baseCoords = this.getBaseCoords();
        var alphaTimesAng = alpha * angularSpeed;
        
        for (var i = 0; i < this.items.length; i++) {
            
            var baseCoord = baseCoords[i];
            var tmp = ((this.center.y + baseCoord.y - this.radius) *  Math.pow(Math.cos(alphaTimesAng), direction));
            var y = this.center.y;
            if (isUp && this.direction != 1) {
                y -= tmp;
            }
            else {
                y += tmp;
            }
            var x = baseCoord.x + this.center.x;
            this.items[i].setPosition(new Point(x, y));
        }
        alpha = this.getIncrementedAlpha(alpha, angularSpeed, alphaTimesAng);
    }
    
    this.perform = function() {
        this.moveItems();
        this._nextFrame();
    }
    
}


// +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ LINE +++++++++++++++++++++++++++++++++++++++++++++++++++++++++
StrategyLine.prototype = new ItemSingleStrategy;
StrategyLine.prototype.STRATEGYNAME = "Simple Line Strategy";

function StrategyLine(item, startPoint, targetPoint, steps) {

    this.base = ItemSingleStrategy;
    this.base(item, steps);
    
    this.start = startPoint;
    this.end = targetPoint;
    
    this.totDistance = this.start.getDistance(this.end);
    
    this.alpha = Math.acos(Math.abs(this.start.x - this.end.x) / this.totDistance);
    
    if (isNaN(this.alpha)) {
        this.alpha = 0;
    }
    
    this.xMultiplier = 1;
    this.yMultiplier = 1;
    
    this.perform = function() {
        this.moveItem();
        this._nextFrame();
    }
    
    this.moveItem = function() {
        var position = _getPointOnHypotenuse(this.getFrame(), this.getSteps(), this.alpha, this.totDistance, this.xMultiplier, this.yMultiplier);
        this.item.setPosition(this.start.add(position));
    }
    
    this.toString = function() {
        return "Strategy: " + this.STRATEGYNAME + this.getStringBase() + this.getStringFrame() + this.getStringItem() + " line: " + this.start + " - " + this.end + "\n";
    }
    
    // ++++++++++++++++++++++++++++++++++++
    // +++++++++++++ PRIVATES +++++++++++++
    // ++++++++++++++++++++++++++++++++++++
    
    function _getPointOnHypotenuse(frame, steps, alpha, distance, xMult, yMult) {
    
        var hypotenuse = frame * distance / steps;
        
        var y = Math.sin(alpha) * hypotenuse * yMult;
        var x = Math.cos(alpha) * hypotenuse * xMult;
        
        if (isNaN(x) || isNaN(y)) {
            //throw new Error("This shouldn't happen - " + "x: " + x + ", y: " + y);
            return new Point(0, 0);
        }
        
        return new Point(x, y);
    }
    
    this._computeTemps = function() {
    
        var position = this.item.getPosition();
        this.totDistance = this.start.getDistance(this.end);
        if (this.totDistance == 0) {
            this.running = false;
            this.fullStop();
            return;
        }
        this.alpha = Math.acos(Math.abs(this.start.x - this.end.x) / this.totDistance);
        //  		if(isNaN(this.alpha)){
        //  			this.alpha = 0;
        //  		}
        if (this.start.x > this.end.x) 
            this.xMultiplier = -1;
        if (this.start.y > this.end.y) 
            this.yMultiplier = -1;
    }
    
    this._computeTemps();
    this.setLoopsToDo(1);
}

CatchAndReplace.prototype = new ItemSingleStrategy;
CatchAndReplace.prototype.STRATEGYNAME = "Catch and Replace";

function CatchAndReplace(followee, item, steps) {

    this.base = ItemSingleStrategy;
    this.base(item, steps);
    this.followee = followee;
    this.lastDistance = Number.MAX_VALUE;
    this.catchListener = null;
    
    this.perform = function() {
    
        if (this.item.destroyed) {
        
            this.fullStop();
            return;
        }
        
        if (this.followee.destroyed) {
        
            this.fullStop();
            return;
        }
        
        var start = this.item.getPosition() || new Point(0, 0);
        var end = this.followee.getPosition();
        var distance = start.getDistance(end);
        //alert("Start\n" + start + "\nend\n" + end);
        
        if (distance <= 1) {
            //alert("Start\n" + start + "\nend\n" + end + "\nReached target.");
            this.reachedTarget();
            return;
        }
        
        this.tweakVelocity(distance);
        
        var alpha = Math.acos(Math.abs(start.getX() - end.getX()) / distance);
        
        var hypotenuse = this.steps;
        
        if (distance <= this.steps) {
            hypotenuse = distance;
        }
        
        var x = Math.cos(alpha) * hypotenuse;
        var y = Math.sin(alpha) * hypotenuse;
        
        if (start.getX() > end.getX()) 
            x = -x;
        if (start.getY() > end.getY()) 
            y = -y;
        
        var newPos = start.add(new Point(x, y));
        this.lastDistance = newPos.getDistance(end);
        
        if (this.lastDistance <= 1) {

            this.item.setPosition(end);
            this.reachedTarget();
            return;
        }
        
        this.item.setPosition(newPos);
        this._nextFrame();
    }
    
    this.tweakVelocity = function(distance) {
    
        if (distance > this.lastDistance) {
            this.steps++;
        }
    }
    
    this.removeItem = function() {
        
        //alert("Attempt to remove item form catcher");
        var candidate = this.followee.movementStrategy;
        
        if(candidate) {
            candidate.removeItem(this.followee);
        }
        
        this.followee.destroy();
        return this.removeItem0();
    }
    
    this.setCatchListener = function(catchListener) {
    
        this.catchListener = catchListener;
    }
    
    this.reachedTarget = function() {
    
        var strategy = this.followee.movementStrategy;
        
        if (strategy != null) {
        
            if (!strategy.replaceItem(this.followee, this.item)) {
                //alert("Could not replace " + this.followee + " with " + this.item);
                this.followee.setBgColor("#ffcc00");
            }
            //
        }
        
        this.followee.destroy();
        
        if(this.catchListener && typeof this.catchListener == "function") {
            this.catchListener(this.followee, this.item, strategy);
        }
        
        //this.followee.replaceElement(follower.removeElement());
        //alert("Replaced\n" + this.followee + "\nwith\n" + follower);
        this.fullStop();
    }
}

// +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ POLYGON +++++++++++++++++++++++++++++++++++++++++++++++++++++++++
StrategyPolygon.prototype = new ItemSingleStrategy;
StrategyPolygon.prototype.STRATEGYNAME = "Simple Polygon Strategy";
/**
 * Item will move to first point of polygon and then move to all other points given.
 * @param {ImageItem} item - the item to move
 * @param {[Point || Strategy]} points - they make up the polygon, or / and strategies that stop themself somehow (if they are endless, no following movements get executed)
 * @param {bool} loopEndless - if false: animation will stop when reached the last point. Otherwise the animation will start again at first point given.
 * @param {Number} steps - time in frames to do one loop, is optional (default is 100) you can also use setStepsPerSegment()
 */
function StrategyPolygon(item, points, loopEndless, steps) {

    this.base = ItemSingleStrategy;
    this.base(item, steps);
    
    this.actLine;
    this.isLoop = false;
    
    var steps = steps ? steps : 100;
    
    var firstLineOnPoly;
    var distance = 0;
    var stepsPerDistance = 0;
    
    this.moveItem = function() {
        if (!this.actLine.isDone()) {
            this.actLine.perform();
        }
        else {
            if (this.actLine.getNextStrategy() != null) {
                this.actLine = this.actLine.getNextStrategy();
                this.actLine.resetDone();
            }
        }
    }
    
    this.isDone = function() {
        if (this.isLoop) 
            return false;
        else 
            return (this.actLine.getNextStrategy() == null && this.actLine.isDone());
    }
    
    this.perform = function() {
        this.moveItem();
        this._nextFrame();
    }
    
    /**
     * Here you may define individual steps for each segment of this polygon.
     * You are allowed to give less steps than poly has lines. Then only the first n lines will have non-default steps.
     * @param {[Number]} stepsArray - an array of steps with same length as points of this polygon
     */
    this.setStepsPerSegment = function(stepsArray) {
    
        var lineToAlter = firstLineOnPoly;
        for (var i = 0; i < stepsArray.length; i++) {
            if (lineToAlter != null) {
                lineToAlter.setSteps(stepsArray[i]);
                lineToAlter = lineToAlter.getNextStrategy();
                
            }
            else {
                throw new Error("setStepsPerSegment(stepsArray): given array is too long!");
            }
        }
    }
    
    this.strategyChainToString = function() {
        var str = "";
        var line = firstLineOnPoly;
        while (line.getNextStrategy() != null) {
            str += "Move: " + line.start + " -> " + line.end + " @steps:" + line.getSteps() + "\n";
            line = line.getNextStrategy();
        }
        str += "Move: " + line.start + " -> " + line.end + " @steps:" + line.getSteps() + "\n";
        if (line.getNextStrategy() == firstLineOnPoly) {
            str += " --> back to first, do loop!\n";
        }
        return str;
    }
    
    this.toString = function() {
        return "chain of points:\n" + " with total distance: " + distance + "\n" + this.strategyChainToString();
    }
    
    // PRIVATE:
    this._computePolygonDistance = function() {
        distance = 0;
        var realPoints = new Array();
        for (var i = 0; i < points.length; i++) {
            if (points[i] instanceof Point) {
                realPoints.push(points[i]);
            }
        }
        
        for (var i = 1; i < realPoints.length; i++) {
            distance += realPoints[i - 1].getDistance(realPoints[i]);
        }
        if (this.isLoop) {
            distance += realPoints[realPoints.length - 1].getDistance(realPoints[0]);
        }
        stepsPerDistance = steps / distance;
    }
    
    this._generateLines = function(points) {
    
        var amount = points.length;
        var actPoint = this.item.getPosition();
        var strategies = new Array();
        for (var i = 0; i < amount; i++) {
            if (points[i] instanceof Point) {
                var distOfSegment = actPoint.getDistance(points[i]);
                var strategy = new StrategyLine(item, actPoint, points[i], Math.round(stepsPerDistance * distOfSegment));
                strategies.push(strategy);
                actPoint = points[i];
            }
            else if (points[i].perform instanceof Function) {
                strategies.push(points[i]);
            }
            else 
                throw new Error("must put Point's or Strategies as points in constructor, " + points[i] + " is illegal");
        }
        if (points[points.length - 1] instanceof Point) {
            strategies.push(new StrategyLine(item, actPoint, points[points.length - 1], Math.round(stepsPerDistance * distOfSegment)));
        }
        else {
            strategies.push(points[points.length - 1]);
        }
        this.actLine = strategies.shift();
        this._attachPolyLines(strategies);
    }
    
    this._attachPolyLines = function(strategies) {
        var act = this.actLine;
        for (var i = 0; i < strategies.length; i++) {
            var nextLine = strategies[i];
            if (i == 0) {
                firstLineOnPoly = nextLine;
            }
            act.setNextStrategy(nextLine);
            act = nextLine;
        }
    }
    
    this._computePolygonDistance();
    this._generateLines(points);
}

// +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ PAUSE +++++++++++++++++++++++++++++++++++++++++++++++++++++++++
StrategyPause.prototype = new ItemSingleStrategy;
StrategyPause.prototype.STRATEGYNAME = "Simple PAUSE Strategy";
/**
 * Item will do nothing for the next n steps.
 * @param {ImageItem} item - the item to move
 * @param {Number} steps - time in frames to pause
 * @param {Function} funcOnBegin - a pointer to a function invoked at beginning of this strategy
 * @param {Function} funcOnEnd - a pointer to a function invoked at the end of this strategy
 */
function StrategyPause(item, steps, funcOnBegin, funcOnEnd) {

    this.base = ItemSingleStrategy;
    this.base(item, steps);
    this.setLoopsToDo(1);
    
    this.onStart = funcOnBegin;
    this.onEnd = funcOnEnd;
    
    
    this.hasNothingToDo = function() {
        return false;
    }
    
    this.didOnStart = false;
    this.didOnEnd = false;
    
    this.perform = function() {
    
        if (!this.didOnStart) {
            if (this.onStart && this.onStart instanceof Function) {
                this.onStart();
            }
            this.didOnStart = true;
        }
        
        this._nextFrame();
        
        if (this.isDone()) {
            if (!this.didOnEnd) {
                if (this.onEnd && this.onEnd instanceof Function) {
                    this.onEnd();
                }
                this.didOnEnd = true;
            }
        }
    }
    this.reset = function() {
        this.didOnStart = false;
        this.didOnEnd = false;
        this.resetDone();
        this.resume();
    }
}

// +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ PAUSE +++++++++++++++++++++++++++++++++++++++++++++++++++++++++
ExecuteOnce.prototype = new StrategyBase;
ExecuteOnce.prototype.STRATEGYNAME = "ExecuteOnce";
/**
 * Item will do nothing for the next n steps.
 * @param {ImageItem} item - the item to move
 * @param {Number} steps - time in frames to pause
 * @param {Function} funcOnBegin - a pointer to a function invoked at beginning of this strategy
 * @param {Function} funcOnEnd - a pointer to a function invoked at the end of this strategy
 */
function ExecuteOnce(func) {

    this.base = StrategyBase;
    this.base();
    
    this.func = func;
    this.performed = false;
    
    this.perform = function() {
    
        if (!this.performed) {
            if (this.func && this.func instanceof Function) {
                this.func();
            }
            
            this.performed = true;
        }
    }
    
    this.hasNothingToDo = function() {
        return this.performed;
    }
    
    this.reset = function() {
        this.performed = false;
        this.resetDone();
        this.resume();
    }
}

function Executor(func) {

    this.base = StrategyBase;
    this.base();
    this.func = func;
    
    this.perform = function() {
    
        if(!this.func()) {
            this.fullStop();
        }
    }
    
    this.hasNothingToDo = function() {
        return false;
    }
    
    this.reset = function() {
        this.resetDone();
        this.resume();
    }
}

StrategyMoveTo.prototype = new ItemSingleStrategy;
StrategyMoveTo.prototype.STRATEGYNAME = "MOVETO Strategy";
function StrategyMoveTo(item, targetPoint, isTargetAbsolute, steps) {

    this.base = ItemSingleStrategy;
    this.base(item, steps);
    
    this.target = targetPoint;
    this.isTargetAbsolute = isTargetAbsolute; // if not the target is computed relative to items current point (current means the time when perform is invoked the first time)
    this._nextHolder = null;
    
    this.perform = function() {
        var destinationP = this._getDestinationPoint();
        this.line = new StrategyLine(this.item, this.item.getPosition(), destinationP, this.getSteps());
        if (this._nextHolder != null) {
            this.line.setNextStrategy(this._nextHolder);
            this._nextHolder = null;
        }
    }
    
    this._getDestinationPoint = function() {
        if (isTargetAbsolute) {
            return this.target;
        }
        else {
            this.target = this.item.getPosition().add(this.target);
        }
        return this.target;
    }
    
    this.isDone = function() {
        return this.line != null;
    }
    
    this.setNextStrategy = function(strategy) {
        this._nextHolder = strategy;
    }
    this.getNextStrategy = function() {
        if (this.line == null) {
            return this._nextHolder;
        }
        else {
            var result = this.line;
            this.line = null;
            return result;
        }
    }
    this.hasNothingToDo = function() {
        return false;
    }
}

StrategyChain.prototype = new StrategyBase;
StrategyChain.prototype.STRATEGYNAME = "Strategy Chain";
function StrategyChain(strategies) {

    this.base = StrategyBase;
    this.base();
    
    this.strategies = strategies;
    this.currentStrategy = this.strategies.shift();
    
    this.perform = function() {
        if (!this.currentStrategy.isDone()) {
            this.currentStrategy.perform();
        }
        else {
            // current is done
            var next = this.currentStrategy.getNextStrategy();
            var hasNext = next != null
            if (!hasNext && this.strategies.length > 0) {
                next = this.strategies.shift();
                hasNext = true;
            }
            if (!hasNext) {
                this.fullStop();
                return;
            }
            var items = this.currentStrategy.removeAllItems();
            next.setItems(items);
            next.reset();
            
            this.currentStrategy = next;
        }
    }
    
    this.removeAllItems = function() {
        return this.currentStrategy.removeAllItems();
    }
    
    this.hasNothingToDo = function() {
        return this.currentStrategy.hasNothingToDo();
    }
}

function StrategyBuilder() {
  
  var steps, radius, secondaryRadius, secondarySteps, angularSpeed;
  var radiusMax, radiusMin, mainRadiusIncr, secondaryRadiusIncr, secondarySpeed;
  
  this.createStrategy = function(strategyName) {
      
      checkRequired();
      
      switch(strategyName) {
          
          case StrategyBuilder.CIRCLE :
              return new StrategyCircle(steps, center, radius, direction); 
          case StrategyBuilder.ROTATION_Y :
          case StrategyBuilder.ROTATION_X :
          case StrategyBuilder.HORIZONTAL_HOP :
          case StrategyBuilder.VERTICAL_HOP :
          case StrategyBuilder.TWISTER :
              checkAngularSpeed();
              return eval("new " + strategyName + "(steps, center, radius, angularSpeed, direction)");
          case StrategyBuilder.ELLIPSE :
              checkAvailable(secondaryRadius, "Secondary radius");
              checkAvailable(secondarySpeed, "Secondary steps");
              checkAvailable(radiusMax, "Max radius");
              checkAvailable(radiusMin, "Min radius");
              checkAvailable(mainRadiusIncr, "Main radius incrementor");
              checkAvailable(secondaryRadiusIncr, "Secondary radius incrementor");
              
              return new Ellipse(steps,
                 center, 
                 radius, 
                 direction,
                 secondarySpeed,
                 secondaryRadius,
                 mainRadiusIncr,
                 secondaryRadiusIncr, 
                 radiusMin, 
                 radiusMax);
               
          default :
              throw new Error("Unknown strategy: " + strategyName);
      }
  }
    
  function checkRequired() {
    
      checkAvailable(center, "Center");
      checkAvailable(radius, "Radius");
      checkAvailable(steps, "Steps");  
      checkAvailable(direction, "Direction");          
  }
  
  function checkAngularSpeed() {
      
      checkAvailable(angularSpeed, "Angular speed");              
  }
  
  function checkAvailable(candidate, name) {
      
      if(candidate == null || typeof(candidate) == "undefined") {
          throw new Error(name + " not set");
      }            
  }
  
  function checkIsNumber(candidate, name) {
      
      if(isNaN(candidate)) {
          throw new Error(name + " is not a number, but " + candidate);
      }            
  }
  
  function checkRange(candidate, name, range) {
      
      if(!range.isWithin(candidate)) {
          throw new Error(name + " must be between " + range.getMin()
                        + " and " + range.getMax() + ", but is " + candidate);
      }            
  }

  this.setCenter = function(c) {
      center = c;
      return this;
  }
  
  this.setDirection = function(d) {
      direction = d;
      return this;
  }
  
  this.setAngularSpeed = function(a) {
      checkIsNumber(a, "Angular speed");
      angularSpeed = a;
      return this;
  }
  
  this.setSteps = function(s) {
      checkIsNumber(s, "Steps");
      steps = s;
      return this;
  }
  
  this.setRadius = function(r) {
      checkIsNumber(r, "Radius");
      radius = r;
      return this;
  }
  
  this.setSecondaryRadius = function(r) {
      checkIsNumber(r, "Secondary radius");
      secondaryRadius = r;
      return this;
  }
  
  this.setSecondarySpeed = function(s) {
      checkIsNumber(s, "Secondary speed");
      checkRange(s, "Secondary speed", StrategyBuilder.GREATER_EQUAL_ZERO);
      secondarySpeed = s;
      return this;
  }
  
  this.setRadiusMax = function(r) {
      checkIsNumber(r, "Max radius");
      checkRange(r, "Max radius", StrategyBuilder.GREATER_EQUAL_ZERO);
      radiusMax = r;
      return this;
  } 
  
  this.setRadiusMin = function(r) {
      checkIsNumber(r, "Min radius");
      checkRange(r, "Min radius", StrategyBuilder.GREATER_EQUAL_ZERO);
      radiusMin = r;
      return this;
  }
  
  this.setMainRadiusIncrement = function(i) {
      checkIsNumber(i, "Main radius incrementor");
      mainRadiusIncr = i;
      return this;
  } 
  
  this.setSecondaryRadiusIncrement = function(i) {
      checkIsNumber(i, "Secondary radius incrementor");
      secondaryRadiusIncr = i;
      return this;
  }  
       
}

StrategyBuilder.CIRCLE   = "StrategyCircle";
StrategyBuilder.ROTATION_Y = "StrategyRotationY";
StrategyBuilder.ROTATION_X = "StrategyRotationX";
StrategyBuilder.TWISTER = "StrategyTwister";
StrategyBuilder.ELLIPSE = "Ellipse";
StrategyBuilder.HORIZONTAL_HOP = "StrategyHorizontalHop";
StrategyBuilder.VERTICAL_HOP = "StrategyVerticalHop";

StrategyBuilder.STRATEGY_NAMES = [
    StrategyBuilder.ELLIPSE,
    StrategyBuilder.CIRCLE, 
    StrategyBuilder.ROTATION_Y, 
    StrategyBuilder.TWISTER, 
    StrategyBuilder.ROTATION_X, 
    StrategyBuilder.HORIZONTAL_HOP, 
    StrategyBuilder.VERTICAL_HOP
  ];

StrategyBuilder.GREATER_ZERO = new SimpleRange(1, Number.MAX_VALUE);
StrategyBuilder.GREATER_EQUAL_ZERO = new SimpleRange(0, Number.MAX_VALUE);













