var FrameType = {
	NONE: 0, RTS: 1, CTS: 2, DF: 3, ACK: 4
};
var CarrierType = {
	FREE: 0, BUSY: 1, NAV: 2
};
var difsTimeoutDefault = 30;
var sifsDefaultLength = 10;
var ctsLength = 10;
var dataLength = 80;
var waitForReplyTimeoutDefault = sifsDefaultLength+ctsLength+10;
var backoffTimeFactor = 1;
var CWmin = 20;
var CWmax = 400;

var pkt_id = 0;

var nodeNames = [
    "station A",
    "station B",
    "station C",
    "access point"
];


function Logger(){
	var maxDelay = 100;
	this.data = new Array(maxDelay);
	for(var i = 0; i < this.data.length; i++)
		this.data[i] = [];
	
	this.getLogs = function(){
		return this.data[0];
	};
	
	this.pushStr = function(delay, str){
		if(delay > maxDelay-1 || delay < 0)
			console.error("invalid delay");
		this.data[delay].push(str);
	};
	
	this.next = function(){
		this.data.shift(); // remove first
		this.data.splice(this.data.length, 0, 1); // add new element
		this.data[this.data.length-1] = new Array();
	};
	
	this.log = function(msg, who){
		var str = who.name + ": "+msg;
		
		this.pushStr(0, str);
	};
	
	this.logPktSent = function(delay, who, pkt){
		var str = who.name + ": sent "+ pkt.getTypeName() + "("+ pkt.id +") to " + nodeNames[pkt.to];
		this.pushStr(delay, str);
	};
	
	this.logNewNav = function(delay, who, pkt){
		var str = who.name + ": updated nav to "+pkt.data.nav+" from packet "+pkt.getTypeName() + "("+ pkt.id +") by "+ nodeNames[pkt.from];
		
		this.pushStr(delay, str);
	};
}


function Simulation(){
	var self = this;
	var interval = 0;
	var step = 0;
	
	var draw = new Draw(document.getElementById('canvas'), 2, new Array(0, 104, 210, 310), nodeNames, document.getElementById('tooltip'));
	var stations = new Array();
	this.running = false;
	this.logger = new Logger();
	
	// params
	this.hiddenTerminal = false;
	
	this.setParams = function(hiddenTerminal){
		this.hiddenTerminal = hiddenTerminal;
	};
	
	
	
	this.init = function(){
		this.hiddenTerminal;
		this.uninit();
		draw.clear();
		draw.drawDescription();
		
		var senderA = new macSender(0, nodeNames[0], this.logger);
		var senderB = new macSender(1, nodeNames[1], this.logger);
		var senderC = new macSender(2, nodeNames[2], this.logger);
		var receiver = new macReceiver(3, nodeNames[3], this.logger);
		
		stations = new Array(senderA, senderB, senderC, receiver);
		
		if(!this.hiddenTerminal){
			// all stations hear each other
			for(var i = 0; i < stations.length; i++){
				for(var j = 0; j < stations.length; j++){
					if(i == j)
						continue;
					stations[i].registerHost(stations[j], 1);
				}
			}
			console.log("NO hiddenTerminal => connect all stations hear each other");
		}
		else{
			// stations are only connected to receiver
			for(var i = 0; i < stations.length-1; i++){
				stations[i].registerHost(receiver, 1);
				receiver.registerHost(stations[i], 1);
			}
			console.log("hiddenTerminal => connect all stations to receiver");
		}
		
		
		// register click handlers
		$('#simulation tbody button.enqueue').each(function(index, element){
			$(element).click(function(){
				stations[index].fromAbove(dataLength, receiver.id);
			});
		});
		
		
		pkt_id = 0;
	};
	
	this.start = function(){
		(function animloop(){
			interval = requestAnimFrame(animloop);
			self.run();
	    })();
		
		
		this.running = true;
		$('#cfg-start').attr('value', 'pause');
		
		draw.disableTooltips();
		
	};
	this.pause = function(){
		//window.clearInterval(interval);
		cancelRequestAnimFrame(interval);
		
		this.running = false;
		$('#cfg-start').attr('value', 'start');
		
		draw.enableTooltips();
	};
	
	
	this.toggleStartPause = function(){
		if(this.running)
			this.pause();
		else
			this.start();
		return this.running;
	};
	
	this.run = function(){
		var calcInThisStep = (step%2 == 0);
		
		if(calcInThisStep){
			for(var i = 0; i < stations.length; i++)
				stations[i].step();
		}
		
		
		// redraw
		for(var i = 0; i < stations.length; i++){
			draw.drawSlice(i, stations[i]);
			
			// number of pkts waiting
			$('#simulation tbody tr > td.queue').each(function(index, element){
				$(element).html(stations[index].getQueueLength());
			});
			
			// number of collisions
			$('#simulation tbody tr > td.backoff').each(function(index, element){
				$(element).html(stations[index].getBackoff());
			});
		}
		
		if(calcInThisStep){
			for(var i = 0; i < stations.length; i++)
				stations[i].next();
			
			// logs only at calc steps
			var logs = this.logger.getLogs();
			for(var i = 0; i < logs.length; i++){
				$('#logger').prepend('<li>'+logs[i]+'</li>');				
			}
			
			
			
			this.logger.next();
		}
		
		
		draw.shiftX(-1);
		step++;
	};
	
	this.uninit = function(){
		this.pause();
		
		// unregister click handlers
		$('#simulation tbody button.enqueue').each(function(index, element){
			$(element).off('click');
		});
		
		$('#logger').empty();
	};
	
	
	$('#cfg-start').click(function(){
		self.toggleStartPause();
	});
	$('#canvas').click(function(){
		self.toggleStartPause();
	});
}

function Draw(canvas, pixelPerSlice, positions, nodeNames, tooltip){
	this.positions = positions;
	this.nodeNames = nodeNames;
	this.height = $(canvas).height();
	this.width = $(canvas).width();
	this.widthVisible = this.width-100;
	this.pixelPerSlice = pixelPerSlice;
	
	this.frameTypeColors = [
		'#ffffff', // none
		'#00adef', // rts
		'#004965', // cts
		'#000000', // df #0dc800
		'#076500' // ack
	];
	this.carrierSensingColors = [
		'#cacaca', // free
		'#f80100', // busy
		'#fbce00' // nav
	];
	var countDownColor = '#7f7f7f';
	
	if (canvas.getContext == false)
		return;
	var c = canvas.getContext('2d');
	
	function rgbToHex(r, g, b) {
	    if (r > 255 || g > 255 || b > 255)
	        throw "Invalid color component";
	    return ((r << 16) | (g << 8) | b).toString(16);
	}
	
	var mouse_x = 0;
	var mouse_y = 0;
	var tooltip_text = "";
	var tooltipEnabled = false;
	this.setTooltipText = function(){
		var p = c.getImageData(mouse_x, mouse_y, 1, 1).data; 
	    var hex = "#" + ("000000" + rgbToHex(p[0], p[1], p[2])).slice(-6);
	    //console.log(hex);
	    
	    var text = "&nbsp;";
	    
	    switch(hex){
	    case this.frameTypeColors[FrameType.RTS]:
	    	text = "RTS (request to send)";
	    	break;
	    case this.frameTypeColors[FrameType.CTS]:
	    	text = "CTS (clear to send)";
	    	break;
	    case this.frameTypeColors[FrameType.DF]:
	    	text = "data frame";
	    	break;
	    case this.frameTypeColors[FrameType.ACK]:
	    	text = "ACK";
	    	break;
	    	
	    case this.carrierSensingColors[CarrierType.FREE]:
	    	text = "carrier free";
	    	break;
	    case this.carrierSensingColors[CarrierType.NAV]:
	    	text = "channel considered busy (NAV)";
	    	break;
	    case this.carrierSensingColors[CarrierType.BUSY]:
	    	text = "carrier busy";
	    	break;
	    case this.carrierSensingColors[CarrierType.NAV]:
	    	text = "channel considered busy (NAV)";
	    	break;
	    	
	    case countDownColor:
	    	text = "countdown";
	    	break;
	    }
	    
	    if(tooltip_text != text){
	    	tooltip_text = text;
	    	
	    	$(tooltip).html(text);
	    }
	};
	
	
	this.enableTooltips = function(){
		if(tooltipEnabled)
			return;
		
		var self = this;
		$(canvas).mousemove(function(e){
			var offset = $(this).offset();
			mouse_x = Math.floor(e.pageX - offset.left);
			mouse_y = Math.floor(e.pageY - offset.top);
			
			self.setTooltipText(); 
		});
		tooltipEnabled = true;
	};
	this.disableTooltips = function(){
		if(!tooltipEnabled)
			return;
		$(canvas).unbind("mousemove");
		tooltipEnabled = false;
		
		$(tooltip).html("&nbsp;");
	};
	
	// slice height: 88;
	this.drawSlice = function(index, station){
		var y = this.positions[index];
		var x = this.widthVisible-this.pixelPerSlice;
		
		var frame = station.getFrameType();
		var carrier = station.getCarrierType();
		var isCountingDown = station.getIsCountingDown();
		var frameDesc = station.getFrameDesc();
		
		c.save();
		
		y += 14; // top border
		
		if(frameDesc != ""){
			c.textBaseline = 'bottom';
			c.textAlign = 'left';
			c.font = '12px sans-serif';
			c.fillText(frameDesc, this.widthVisible+1, y);
			
			c.fillStyle = countDownColor;
			c.fillRect(this.widthVisible+1, y+18, 2, 2);
		}
		
		
		// frame
		if(isCountingDown){
			c.fillStyle = countDownColor;
			c.fillRect(x, y+18, this.pixelPerSlice, 32);
		}
		else{
			c.fillStyle = this.frameTypeColors[frame];
			c.fillRect(x, y, this.pixelPerSlice, 50);
		}
		
		y += 56+4;
		
		//carrier
		c.fillStyle = this.carrierSensingColors[carrier];
		c.fillRect(x, y, this.pixelPerSlice, 20);
		
		c.restore();
	};
	
	this.clear = function(){
		c.clearRect(0, 0, this.width, this.height);
	};
	
	this.drawDescription = function(){
		// draw node desc
		c.save();
		c.fillStyle = '#000';
		c.font = 'bold 12px sans-serif';
		c.textBaseline = 'bottom';
		c.textAlign = 'right';
		var y;
		for(var i = 0; i < this.positions.length; i++){
			y = this.positions[i]+20;
			
			c.font = 'bold 12px sans-serif';
			c.fillText('frame > ', this.widthVisible, y+25);
			
			c.font = 'bold 13px sans-serif';
			c.fillText(this.nodeNames[i], this.widthVisible-60, y+45);
			
			y += 56+4;
			c.font = 'bold 12px sans-serif';
			c.fillText('channel > ', this.widthVisible, y+10);
		}
		c.restore();
	};

	this.shiftX = function(shift){
		var d = c.getImageData(0, 0, this.width, this.height);
		//c.clearRect(0, 0, this.width, this.height);
		c.putImageData(d, shift, 0);
		
		if(tooltipEnabled)
			this.setTooltipText();
	};
	
	this.nextSlice = function(){
		this.shiftX(-this.pixelPerSlice);
	};
}




function Pkt(type, length, part, from, to){
	this.id = -1;
	this.type = type;
	this.length = length;
	this.part = part;
	
	if(part > length || length < 1 || part < 1){
		console.error("no valid pkt");
	}
	//if(part == 1)
	//	this.id = pkt_id++;
	
	this.from = from;
	this.to = to;
	
	this.data = {};
	
	this.isTransmitted = function(){
		return this.length == this.part;
	};
	
	this.getTypeName = function(){
		switch(this.type){
		case FrameType.RTS: 
				return 'RTS';
		case FrameType.CTS: 
			return 'CTS';
		case FrameType.DF: 
			return 'DF';
		case FrameType.ACK: 
			return 'ACK';
		}
	};
}

// each host get an individual carrier
function Carrier(){
	this.pkts = Array(1024); // array of arrays of packets 
	this.currentPos = 256;
	
	for(var i = 0; i < this.pkts.length; i++)
		this.pkts[i] = new Array();
	
	// tests if the channel is currently free
	this.isFree = function(){
		return this.pkts[this.currentPos].length == 0;
	};
	
	// if there is a collision of packages at the current position
	this.collision = function(){
		return this.pkts[this.currentPos].length > 1;
	};
	
	this.hasPkt = function(){
		return this.pkts[this.currentPos].length > 0;
	};
	
	this.getPkt = function(logger){
		if(this.pkts[this.currentPos].length != 1){
			return null; // collision or no pkt
		}
		
		
		var pkt = this.pkts[this.currentPos][0];
		
		if(pkt.part != pkt.length){
			return null; // not the last part
		}
		
		// check if there were any collisions
		for(var i = this.currentPos-1; i > this.currentPos-pkt.length; i--){
			if(this.pkts[i].length > 1){
				return null;
			}
		}
		
		return pkt;
	};
	
	this.next = function(){
		this.pkts.shift(); // remove first
		this.pkts.splice(this.pkts.length, 0, 1); // add new element
		this.pkts[this.pkts.length-1] = new Array();
	};
	
	// adds a pkt to this carrier from a host that has the given distance; so the pkt will arrive (at currentPos) after n=distance time slots 
	this.transmit = function(pkt, distance){
		if(distance == 0)
			console.error("distance can not be 0");
		
		this.pkts[this.currentPos+distance].push(pkt);
	};
	
	
	this.getFrame = function(from, distance){
		var frames = this.pkts[this.currentPos+distance];
		for(var i = 0; i < frames.length; i++){
			if(frames[i].from == from)
				return frames[i];
		}
		return null;
	};
}





var Node = Class.extend({
	init: function(id, name, logger){ // ctor
		this.id = id;
		this.name = name;
		this.logger = logger;
		
		this.visibleHosts = [
			// {host: obj,
			// distance: int}
		];
		this.carrier = new Carrier();
		
		this.sendPkt_Timeout = 0;
	},
	
	getFrameType: function(){
		// if we are sending; transmission is always to all visilbe hosts 
		if(this.visibleHosts.length > 0){
			var frame = this.visibleHosts[0].host.carrier.getFrame(this.id, this.visibleHosts[0].distance);
			if(frame == null)
				return FrameType.NONE;
			else
				return frame.type;
		}
		return FrameType.NONE;
	},
	
	getIsCountingDown: function(){
		return false;
	},
	
	getFrameDesc: function(){
		// if we are sending; transmission is always to all visilbe hosts 
		if(this.visibleHosts.length > 0){
			var frame = this.visibleHosts[0].host.carrier.getFrame(this.id, this.visibleHosts[0].distance);
			if(frame != null && frame.part == 1)
				return frame.id+"";
			else
				return "";
		}
		return "";
	},
	
	
	getCarrierType: function(){
		console.error("not implemented");
	},
	
	registerHost: function(host, distance){
		this.visibleHosts.push({
			host: host,
			distance: distance
		});
	},
	
	transmitPkt: function(pkt, distance, delay){
		if(pkt.part != 1)
			console.error("unexpected part");
		
		var part;
		for(var i = 0; i < pkt.length; i++){
			part = $.extend(true, {}, pkt);
			part.part = i+1;
			
			this.carrier.transmit(part, delay+distance+i);
		}
	},
	
	sendPkt: function(pkt, delay){
		if(this.pktSent() == false)
			console.error("starting new transmission while other is running");
		
		pkt.id = pkt_id++;
		
		for(var i = 0; i < this.visibleHosts.length; i++){
			this.visibleHosts[i].host.transmitPkt(pkt, this.visibleHosts[i].distance, delay);
		}
		this.transmitPkt(pkt, 0, delay);
		this.sendPkt_Timeout = pkt.length + delay;
		
		this.logger.logPktSent(delay, this, pkt);
	},
	
	pktSent: function(){
		return this.sendPkt_Timeout <= 0;
	},
	
	step: function(){
		console.error("not implemented");
	},
	
	next: function(){
		this.carrier.next();
		this.sendPkt_Timeout--;
	}
});

var macSender = Node.extend({
	init: function(id, name, logger){ // ctor
		this._super(id, name, logger);
		
		this.pktsToSend = [];
		
		this.STATES = {
			waitForData: 0,
			sense: 1,
			sense2: 2,
			transmission: 3,
			transmission2: 4,
			waitForReply: 5,
			backoffDifs: 6,
			backoffDifs2: 7,
			backoffDifs3: 8,
			countingDown: 9
		};
		this.currentState = this.STATES.waitForData;
		
		
		this.numCollisions = 0;
		this.pktToSend = null;
		this.ctsReceived = false;
		this.nav = 0;
		this.countdown = 0;
		this.isCountingDown = false;
		this.waitForReplyTimeout = waitForReplyTimeoutDefault;
		this.difsTimeout = 0;
		this.pkgSent = false;
	},
	
	busy: function(){
		return this.carrier.isFree() == false || this.nav > 0;
	},
	
	getIsCountingDown: function(){
		return this.isCountingDown;
	},
	
	getCarrierType: function(){
		if(this.nav > 0)
			return CarrierType.NAV;
		if(!this.carrier.isFree() || this.getFrameType() != FrameType.NONE)
			return CarrierType.BUSY;
		return CarrierType.FREE;
	},
	
	getQueueLength: function(){
		return this.pktsToSend.length;
	},
	
	getBackoff: function(){
		if(this.countdown <= 0)
			return 0;
		return this.countdown;
	},
	
	fromAbove: function(length, to){
		this.pktsToSend.push({
			length: length,
			to: to
		});
	},
	
	getNewNav: function(){
		var pkt = this.carrier.getPkt();
		if(pkt != null && pkt.from != this.id){
			if((pkt.type == FrameType.CTS || pkt.type == FrameType.RTS)	&& pkt.to != this.id){
				if(pkt.data.nav > this.nav){
					this.nav = pkt.data.nav;
					this.logger.logNewNav(0, this, pkt);
				}
				
				return true;
			}
		}
		return false;
	},
	
	step: function(){
		this.nav--;
		var done = 0;
		this.isCountingDown = false;
		while(done == 0){
			switch(this.currentState){
			case this.STATES.waitForData:
				this.numCollisions = 0;
				this.ctsReceived = false;
				this.pkgSent = false;
				
				this.getNewNav();
				
				if(this.pktsToSend.length == 0){
					done = 1;
				}
				else{
					// prepare pkt
					var info = this.pktsToSend[this.pktsToSend.length-1];
					this.pktToSend = new Pkt(FrameType.DF, info.length, 1, this.id, info.to);
					
					
					this.currentState = this.STATES.sense;
				}
				break;
			
			case this.STATES.sense:
				this.difsTimeout = difsTimeoutDefault;
							
			case this.STATES.sense2:
				this.getNewNav();
				if(this.busy()){
					this.currentState = this.STATES.backoffDifs;
					done = 1;
					break;
				}
				
				if(this.carrier.isFree() && this.countdown <= 0 && this.difsTimeout <= 0){
					this.currentState = this.STATES.transmission;
				}
				else{
					if(this.countdown > 0){
						this.countdown--;
						this.isCountingDown = true;
					}
					else
						this.difsTimeout--;
						
					this.currentState = this.STATES.sense2;
					done = 1;
				}
				break;
				
			case this.STATES.transmission:
				var pkt;
				if(this.ctsReceived == false){
					// send rts
					pkt = new Pkt(FrameType.RTS, ctsLength, 1, this.id, this.pktToSend.to);
					pkt.data.nav = 3*sifsDefaultLength + ctsLength + this.pktToSend.length + 3;
				}
				else{
					pkt = this.pktToSend; // send data frame
				}
				this.sendPkt(pkt, sifsDefaultLength);
				
			case this.STATES.transmission2:
				this.currentState = this.STATES.transmission2;
				
				if(this.pktSent()){
					this.currentState = this.STATES.waitForReply;
					this.waitForReplyTimeout = waitForReplyTimeoutDefault; 
				}
				
				done = 1;
				break;
			
			case this.STATES.waitForReply:
				this.getNewNav();
				this.waitForReplyTimeout--;
				
				var pkt = this.carrier.getPkt(this.logger);
				
				if(pkt != null && (pkt.type == FrameType.CTS || pkt.type == FrameType.ACK) && pkt.from != this.id){
					if(this.ctsReceived == false){
						if(pkt.type != FrameType.CTS || pkt.to != this.id){
							done = 1;
							break;
						}
						
						
						this.ctsReceived = true;
						this.currentState = this.STATES.transmission;
					}
					else{
						if(pkt.type != FrameType.ACK || pkt.to != this.id){
							done = 1;
							break;
						}
						
						this.numCollisions = 0;
						this.currentState = this.STATES.backoffDifs;
						this.pkgSent = true;
						this.pktsToSend.pop();
						break;
					}
				}
				else{
					if(this.waitForReplyTimeout == 0){
						this.numCollisions++;
						this.currentState = this.STATES.backoffDifs;
					}
				}
				
				done = 1;
				break;
				
			case this.STATES.backoffDifs:
				if(this.countdown <= 0){
					var cw = Math.min(Math.max(Math.pow(2, this.numCollisions)-1, CWmin), CWmax);
					this.countdown = getRandom(0, cw)* backoffTimeFactor;
				}
			
			case this.STATES.backoffDifs2:
				this.difsTimeout = difsTimeoutDefault;
				
			case this.STATES.backoffDifs3:
				this.getNewNav();
				
				if(this.busy()){
					this.difsTimeout = difsTimeoutDefault;
					this.currentState = this.STATES.backoffDifs2;
					done = 1;
					break;
				}
				
				// if free wait difs
				if(this.difsTimeout-- > 0){
					this.currentState = this.STATES.backoffDifs3;
					done = 1;
					break;
				}
			
				this.currentState = this.STATES.countingDown;
				break;
				
			case this.STATES.countingDown:
				this.getNewNav();
				
				
				if(this.busy()){
					this.currentState = this.STATES.backoffDifs2;
					done = 1;
					break;
				}
				
				this.isCountingDown = true;
				
				this.countdown--;
				if(this.countdown <= 0){
					if(this.pkgSent == true)
						this.currentState = this.STATES.waitForData; // end of backoff after successfull transmission
					else
						this.currentState = this.STATES.transmission; // end of normal backoff
					this.isCountingDown = false;
				}
				else{
					done = 1;
				}
				
				break;
			}
		} // end while
	},
	
	next: function(){
		this._super();
		
		this.frameDesc = "";
	}
});

var macReceiver = Node.extend({
	init: function(id, name, logger){
		this._super(id, name, logger);
		
		this.STATES = {
			waitForData: 0,
			sendResponse: 1,
			sendResponse2: 2
		};
		this.currentState = this.STATES.waitForData;
		
		this.reservedForId = -1;
		
		this.sendPkt_Timeout = 0;
	},
	
	getIsCountingDown: function(){
		return false;
	},
	
	getCarrierType: function(){
		if(this.carrier.isFree() && this.getFrameType() == FrameType.NONE)
			return CarrierType.FREE;
		return CarrierType.BUSY;
	},
	
	toAbove: function(pkt){
		//console.log("toAbove: "+pkt);
	},

	step: function(){
		var done = 0;
		while(!done){
			switch(this.currentState){
			case this.STATES.waitForData:
				var pkt = this.carrier.getPkt();
				if(pkt == null || pkt.from == this.id){
					done = 1;
					break;
				}
				else{
					this.pktReceived = pkt;
					this.currentState = this.STATES.sendResponse;
					
					break;
				}
			
			case this.STATES.sendResponse:
				if(this.reservedForId != -1 && this.pktReceived.from != this.reservedForId){
					// already waiting for other data;
					this.currentState = this.STATES.waitForData;
					done = 1;
					break;
				}
				
				var pkt = null;
				if(this.pktReceived.type == FrameType.RTS){
					pkt = new Pkt(FrameType.CTS, ctsLength, 1, this.id, this.pktReceived.from);
					pkt.data.nav = this.pktReceived.data.nav-ctsLength-sifsDefaultLength;
					
					this.reservedForId = this.pktReceived.from;
					
					this.sendPkt(pkt, sifsDefaultLength);
				}
				else if(this.pktReceived.type == FrameType.DF){
					pkt = new Pkt(FrameType.ACK, ctsLength, 1, this.id, this.pktReceived.from);
					this.sendPkt(pkt, sifsDefaultLength);
				}
				else{
					this.currentState = this.STATES.waitForData;
					done = 1;
					break;
				}
				
				
			
			case this.STATES.sendResponse2:
				this.currentState = this.STATES.sendResponse2;
				
				if(this.pktSent()){
					this.currentState = this.STATES.waitForData;
					
					if(this.pktReceived.type == FrameType.DF)
						this.reservedForId = -1;
				}
				else
					done = 1;
				
				break;
			}
		}
	},
	
	next: function(){
		this._super();
	}
});