/**
 * @author Patrick Kopp
 */
/**
 * All strategies handled by Animator must have at least the functions of StrategyBase.
 */
function StrategyBase() {

    var done = false;
    var doneWhenPaused = false;
    var running = true;
    var nextStrategy = null;
    
    this.isDone = function() {
        return done;
    }
    /**
     * Force this strategy to be done.
     */
    this.fullStop = function() {
        done = true;
    }

    this.resetDone = function() {
        done = false;
    }
    
    this.isRunning = function() {
        return running;
    }
    
    this.pause = function() {
        
        running = false;

        if (doneWhenPaused) {
            done = true;
        }
    }
    
    this.resume = function() {
        running = true;
    }
    
    this.setDoneWhenPaused = function(bool) {
        doneWhenPaused = bool;
    }
    
    this.isDoneWhenPaused = function() {
        return doneWhenPaused;
    }
    
    this.getNextStrategy = function() {
        return nextStrategy;
    }
    
    this.setNextStrategy = function(strategy) {
        nextStrategy = strategy;
    }
    
    this.getStringBase = function() {
    
        var status = this.isRunning() ? "\nruns, " : "\nis paused, ";
        
        if (this.isDone()) {
            status += "done.";
        } else {
            status += "not done.";
        }
        
        var next = nextStrategy != null ? nextStrategy.STRATEGYNAME : "NONE";
        
        status += "\ndone when paused: " + doneWhenPaused;
        
        return status + "\nnext: " + next + "\n ";
    }
    
    this.toString = function() {
        return "Strategy: " + this.STRATEGYNAME + this.getStringBase();
    }
    
    /**
     * Implement this.perform() to give your strategy a functionality.
     */
    this.perform = function() {
        throw new Error("You must override this method in your strategy.")
    }
    
    this.reset = function() {
        this.resetDone();
        this.resume();
    }
    
    this.hasNothingToDo = function() {
        return true;
    }
}

StrategyBase.prototype.STRATEGYNAME = "ABSTRACT Strategy Base 'Interface to Animator'";

// -------------------------------------------------------------------------------------
// ------------------------------------ FRAME BASED ------------------------------------
// -------------------------------------------------------------------------------------

FrameBasedStrategy.prototype = new StrategyBase;
FrameBasedStrategy.prototype.STRATEGYNAME = "ABSTRACT Strategy Frames";
/**
 * Adds functionality for frame based animations.
 * @param {Number} steps - number of total steps/frames of this strategy
 */
function FrameBasedStrategy(stepsPerLoop) {

    //if(isNaN(stepsPerLoop)) {
    //  throw new Error("Invalid steps: " + stepsPerLoop);
    //}
    
    this.base = StrategyBase;
    this.base();
    
    this.steps = stepsPerLoop;
    
    var frameNo = 1;
    
    var loopsCount = 0;
    var maxLoops = -1; // loop forever
    /**
     * Set number of loops to do from now until this strategy stops itself.
     * @param {Number} noOfLoopsFromNow
     */
    this.setLoopsToDo = function(noOfLoopsFromNow) {
        if (noOfLoopsFromNow < 0) 
            throw new Error("setMaximumLoops() needs positive argument");
        maxLoops = loopsCount + noOfLoopsFromNow;
    }

    this.getLoopsDone = function() {
        return loopsCount;
    }
    
    this.setSteps = function(newSteps) {
        this.steps = newSteps;
    }

    this.getSteps = function() {
        return this.steps;
    }
    
    this.setFrame = function(frame) {
        if (frame < 1 || frame > this.steps) 
            throw new Error("Frames have to be in [1, steps+1]. Can not use setFrame(" + frame + ")");
        frameNo = frame;
    }

    this.getFrame = function() {
        return frameNo;
    }
    
    this.isItemBased = function() {
        return false;
    } 
    
    this.getStringFrame = function() {
        var str;
        if (maxLoops > 0) {
            str = loopsCount + maxLoops;
        }
        else {
            str = "<until stopped>"
        }
        return "steps: " + this.steps +
        "\n act frame: " +
        frameNo +
        "\n loops done: " +
        loopsCount +
        "\n loops to do: " +
        str +
        "\n ";
        
    }
    
    this.reset = function() {
        loopsCount = 0;
        frameNo = 1;
        this.resetDone();
        this.resume();
    }
    
    this.toString = function() {
        return "Strategy: " + this.STRATEGYNAME + this.getStringBase() + this.getStringFrame();
    }
    
    // ++++++++++++++++++++++++++++++++++++
    // +++++++++++++ PRIVATES +++++++++++++
    // ++++++++++++++++++++++++++++++++++++
    
    this._nextFrame = function() {
        frameNo++;
        loopDone = false;
        if (frameNo >= this.steps + 1) {
            frameNo = 1;
            loopsCount++;
            if (maxLoops > 0 && loopsCount >= maxLoops) {
                this.fullStop();
            }
        }
    }
    
    this._normalizedFrame = function(frameGot) {
        while (frameGot >= this.steps + 1) {
            frameGot -= this.steps;
        }
        if (frameGot < 1) 
            frameGot = 1;
        return frameGot;
    }
    
}

// -------------------------------------------------------------------------------------
// ---------------------------- FRAME BASED - ITEMS (plural) ---------------------------
// -------------------------------------------------------------------------------------

ItemsStrategy.prototype = new FrameBasedStrategy;
ItemsStrategy.prototype.STRATEGYNAME = "ABSTRACT Items";

function ItemsStrategy(stepsPerLoop) {

    this.base = FrameBasedStrategy;
    this.base(stepsPerLoop);
    this.items = new Array();
    this.frameOffset = 0;
    
    this.hasNothingToDo = function() {
        return (this.getItemsCount() == 0);
    }
    
    this.addItem = function(item) {
    
        if (item != null) {
        
            this.items.push(item);
            this.updateFrameOffset();
            this.smoothPositionTransition();
        }
    }
    
    this.addItems = function(newItems) {
    
        for (var i = 0; i < newItems.length; i++) {
        
            var item = newItems[i];
            
            if (item != null) {
                this.items.unshift(item);
                item.movementStrategy = this;
            }
        }
        
        this.updateFrameOffset();
        this.smoothPositionTransition();
        
    }
    /**
     * Pauses this strategy if last item is removed.
     */
    this.removeItem = function(itemOrId) {
      
        if(!itemOrId) {
            throw new Error("Null argument");
        }
        
        var itemId = (typeof itemOrId == "string" ? itemOrId : itemOrId.getId());
    
        for (var i = 0; i < this.items.length; i++) {
        
            var item = this.items[i];
            
            if (item.getId() == itemId) {
                this.items.splice(i, 1);
                item.setMovementStrategy(null);
                this.updateFrameOffset();
                this.smoothPositionTransition();
                return item;
            }
        }
        
        return null;
    }
    
    this.replaceItem = function(oldItem, newItem) {
    
        for (var i = 0; i < this.items.length; i++) {
        
            var item = this.items[i];
            
            if (item.getId() == oldItem.getId()) {
                this.items[i] = newItem;
                item.setMovementStrategy(null);
                newItem.setMovementStrategy(this);
                return true;
            }
        }
        
        return false;
    }
    
    /**
     * Pauses this strategy.
     */
    this.removeAllItems = function() {
    
        var retItems = this.items.splice(0);
        
        for (var i = 0; i < retItems.length; i++) {
            var item = retItems[i];
            item.setMovementStrategy(null);
        }
        
        return retItems;
    }
    
    this.smoothPositionTransition = function() {
    
        var len = this.getItemsCount();
        
        for (var i = 0; i < len; i++) {
        
            var item = this.items[i];
            
            if (item.isFollowable()) {
                item.movementStrategy = this; // Just in case it comes from another one
                continue;
            }
            
            var followee = new Followable(item);
            this.items[i] = followee;
            //alert("Added " + followee);
            if (!followee.getPosition()) {
                followee.setPosition(new Point(0, 0));
            }
            followee.movementStrategy = this;
            item.movementStrategy = null;
            
            var line = new CatchAndReplace(followee, item, 1);
            animUtil.getAnimator().registerStrategyAutoUnregister(line); // animUtil is a global object
        }
        
        //alert("Added " + newItems.length + " items");
    }
    
    this.getAllItems = function() {
        return this.items.slice();
    }
    
    this.getItemById = function(itemId) {
    
        for (var i = 0; i < this.items.length; i++) {
        
            var item = this.items[i];
            if (item.getId() == itemId) {
                return item;
            }
        }
        
        return null;
    }
    
    this.getItemsCount = function() {
        return this.items.length;
    }
    
    this.isItemBased = function() {
        return true;
    }    
    
    this.getStringItems = function() {
    
        var itemIds = "";
        for (var i = 0; i < this.items.length; i++) {
            itemIds += this.items[i].getId() + ", ";
        }
        return "items: " + itemIds +
        "\n items total: " +
        this.getItemsCount() +
        "\n ";
    }
    
    this.toString = function() {
        return "Strategy: " + this.STRATEGYNAME + this.getStringBase() + this.getStringFrame() + this.getStringItems();
    }
    
    // OVERRIDDEN:
    this.setSteps = function(newSteps) {
    
        if (isNaN(newSteps)) {
            throw new Error("Invalid steps: " + newSteps);
        }
        
        this.steps = newSteps;
        this.updateFrameOffset();
        this.smoothPositionTransition();
    }
    
    // ++++++++++++++++++++++++++++++++++++
    // +++++++++++++ PRIVATE ++++++++++++++
    // ++++++++++++++++++++++++++++++++++++
    
    /**
     * Private
     */
    this.updateFrameOffset = function() {
    
        var itemCount = this.getItemsCount();
        
        if (itemCount > 0) {
            this.frameOffset = this.steps / itemCount;
        }
    }
}

// -------------------------------------------------------------------------------------
// -------------------------- FRAME BASED - SINGLE ITEM --------------------------------
// -------------------------------------------------------------------------------------

ItemSingleStrategy.prototype = new FrameBasedStrategy;
ItemSingleStrategy.prototype.STRATEGYNAME = "ABSTRACT single Item";

function ItemSingleStrategy(item, stepsPerLoop) {

    this.base = FrameBasedStrategy;
    this.base(stepsPerLoop);
    this.item = item;
    
    if (this.item) {
    
        var strategy = item.movementStrategy;
        
        if (strategy) {
            strategy.removeItem(this.item);
        }
        
        this.item.setMovementStrategy(this);
        
    }
    
    this.hasNothingToDo = function() {
        return (this.item == null);
    }
    
    this.isItemBased = function() {
        return true;
    }    
    
    this.getItem = function() {
        return this.item;
    }
    
    this.addItem = function(item) {
    
        if (this.item != null) {
            this.setItem(item);
        }
        
        else {
        
            if (!this.restOfItems) {
                this.restOfItems = new Array();
            }
            
            this.restOfItems.add(item);
        }
    }
    
    this.setItem = function(item) {
    
        this.item = item;
        
        if (this.item) {
        
            var strategy = item.movementStrategy;
            
            if (strategy) {
                strategy.removeItem(this.item);
            }
            
            this.item.setMovementStrategy(this);
            
        }
    }
    
    this.removeItem = function() {
    
        return this.removeItem0();
    }
    
    this.removeItem0 = function() {
    
        var holder = this.item;
        if (holder != null) {
            holder.movementStrategy = null;
        }
        this.item = null;
        this.fullStop();
        return holder;
    }    
    
    this.removeAllItems = function() {
    
        var result = new Array(this.removeItem());
        if (this.restOfItems && this.restOfItems.length > 0) {
            result.concat(this.restOfItems);
            this.restOfItems = null;
        }
        
        return result;
    }
    
    this.setItems = function(items) {
        this.setItem(items.shift());
        this.restOfItems = items;
    }
    
    this.getAllItems = function() {
        var result = new Array(this.item);
        if (this.restOfItems && this.restOfItems.length > 0) {
            result.concat(this.restOfItems);
        }
        return result;
    }
    
    this.getItemsCount = function() {
        return this.getAllItems().length;
    }
    
    this.getStringItem = function() {
        return "item: " + this.item.getId() + "\n ";
    }
    
    this.toString = function() {
        return "Strategy: " + this.STRATEGYNAME + this.getStringBase() + this.getStringFrame() + this.getStringItem();
    }
    
    // OVERRIDDEN:
    this.setSteps = function(newSteps) {
        this.steps = newSteps;
    }
}

var RotationDirection = {
  
    "CW" : {
      
        "getX" : function (radius, theta) {

            return radius * Math.cos(theta);
        },
        
        "getY" : function (radius, theta) {
            
            return radius * Math.sin(theta);
        },      
      
        "getBaseCoord" : function (radius, fPi) {
            
            var x = this.getX(radius, fPi);
            var y = this.getY(radius, fPi);
            
            return new Point(x, y);
        },
        
        "revert" : function () {
            
            return RotationDirection.CCW;
        },
        
        "toString" : function() {
            
            return "clockwise";
        }
    },
   
    "CCW" : {
      
        "getX" : function (radius, theta) {

            return radius * Math.sin(theta);
        },
        
        "getY" : function (radius, theta) {
            
            return radius * Math.cos(theta);
        }, 
        
        "getBaseCoord" : function (radius, fPi) {
            
            var x = this.getX(radius, fPi);
            var y = this.getY(radius, fPi);
            
            return new Point(x, y);
        },
        
        "revert" : function () {
            
            return RotationDirection.CW;
        },        
        
        "toString" : function() {
            
            return "counterclockwise";
        }     
    }
};

// -------------------------------------------------------------------------------------
// ------------------------ FRAME BASED - ITEMS - CIRCLE BASED -------------------------
// -------------------------------------------------------------------------------------

CircleBasedStrategy.prototype = new ItemsStrategy;
CircleBasedStrategy.prototype.STRATEGYNAME = "ABSTRACT Strategy Circle";

function CircleBasedStrategy(steps, center, radiusX, direction) {

    this.base = ItemsStrategy;
    this.base(steps);
    
    this.center = center;
    this.radius = radiusX;
    
    this.direction = 
      (typeof direction == "object")
      ? direction
      : RotationDirection.CW;
    
    var offsetDeg = CircleBasedStrategy.TWO_PI / this.steps;
    
    this.getCenter = function() {

        return this.center;
    }
    
    this.setCenter = function(center) {

        this.center = center;
    }
    
    this.getRadius = function() {

        return this.radius;
    }
    
    this.setRadius = function(radius) {

        if(isNaN(radius)) {
            throw new Error(radius + ", caller: " + arguments.callee.caller.toString());
        }

        this.radius = Number(radius);
    }
    
    // override:
    this.setSteps = function(newSteps) {
      
        if(isNaN(newSteps)) {
            throw new Error(newSteps + ", caller: " + arguments.callee.caller.toString());
        }
    
        this.steps = Number(newSteps);
        offsetDeg = CircleBasedStrategy.TWO_PI / this.steps;
        this.updateFrameOffset();
        this.smoothPositionTransition();
    }
    
    this.setRotationDirection = function(direction) {
    
        this.direction = direction;
        this.smoothPositionTransition();
    }
    
    this.getRotationDirection = function() {
    
        return this.direction;
    }   
    
    this.getOffsetInDegrees = function() {
    
        return offsetDeg;
    }
    
    this.revertRotationDirection = function() {
    
        this.direction = this.direction.revert();
        this.smoothPositionTransition();
    }
    
    this.getBaseCoords = function(amount) {
    
        var pos = new Array();
        var itemCount = amount || this.getItemsCount();
        
        for (var i = 1; i <= itemCount; i++) {
        
            var frame = i * this.frameOffset + this.getFrame();
            var fPi = frame * offsetDeg;
            pos.push(this.direction.getBaseCoord(this.radius, fPi));
            
        }
        
        return pos;
    }
    
    this.getIncrementedAlpha = function(alpha, angularSpeed, currentValue) {
    
        alpha += (1 / angularSpeed / this.getSteps());
        
        if (currentValue >= CircleBasedStrategy.TWO_PI) { // one amplitude is done, reset
            return 0; // to avoid overflows
        }
        
        return alpha;
    }
    
    this.getStringCircleBase = function() {
        return "center: " + this.center + "\n radius: " + this.radius;
    }
    
    this.toString = function() {
        return "Strategy: " + this.STRATEGYNAME + this.getStringBase() + this.getStringFrame() + this.getStringItems() + this.getStringCircleBase();
    }
}

CircleBasedStrategy.TWO_PI = 2 * Math.PI;


