"use strict";

var dns = require ("dns");
var util = require ("util");
var packets = require ("../packets");
var opcodes = require ("../opcodes");
var errors = require ("../errors");
var knownExtensions = require ("../known-extensions");
var Request = require ("../request");

//States
var REQ_SENT = 0;
var ACK_SENT = 1;
var BLK_SENT = 2;

var ClientRequest = module.exports = function (args){
	Request.call (this, args.globalOptions.address, args.globalOptions.port,
			args.globalOptions.retries, args.globalOptions.extensions.timeout);
	
	this._isRRQ = args.reader;
	this._ipFamily = null;
	this._file = args.file;
	this._globalOptions = args.globalOptions;
	this._opOptions =  args.opOptions || {};
	this._prefixError = "(Server) ";
	this._firstPacket = true;
	this._oackExpected = true;
	this._extensionsRetransmitted = false;
	this._extensionsEmitted = false;
	this._maxDataLength = 4;
	this._blksize = null;
	
	
	var me = this;
	me._lookup (function (){
		//Delay the opening to the next tick (the address could be an ip, hence the
		//callback is called immediately)
		process.nextTick (function (){
			me._open (true);
		});
	});
};

util.inherits (ClientRequest, Request);

ClientRequest.prototype._lookup = function (cb){
	var me = this;
	dns.lookup (this._address, function (error, address, family){
		if (error) return me.onError (new Error ("Cannot resolve the domain name " +
				"\"" + me._address + "\""));
		me._address = address;
		me._ipFamily = family;
		cb ();
	});
};

ClientRequest.prototype._open = function (extensions){
	var me = this;
	
	this._initSocket (null, function (message, rinfo){
		if (me._firstPacket){
			me._firstPacket = false;
			//Reset the timer that was started with the request
			me._requestTimer.reset ();
			
			//Save the remote host with the first packet
			me._address = rinfo.address;
			me._port = rinfo.port;
		}else if (me._address !== rinfo.address || me._port !== rinfo.port){
			//A message is received from a different remote host
			//This could happen when the client sends a request, the server sends a
			//response but the client never receives it, so it timeouts and sends the
			//same request again. The server responds again but the client receives
			//the two server responses: the first is accepted but the latter produces
			//this error because from the point of view of the server it receives two
			//different requests and sends the same file from different ports
			return me._send (packets.error.serialize (errors.ESOCKET));
		}
		
		if (message.length < 2) return me._sendErrorAndClose (errors.EBADMSG);
		
		me._onMessage (message);
	});
	
	//Create and send the RRQ/WRQ message
	//There are 2 possible responses from the server:
	//- If the server doesn't support extensions the file transfer starts
	//- If the server supports extensions, it sends a OACK
	var buffer;
	try{
		if (this._isRRQ){
			buffer = extensions
					? packets.rrq.serialize (this._file, this._globalOptions,
							this._opOptions)
					: packets.rrq.serialize (this._file);
		}else{
			buffer = extensions
					? packets.wrq.serialize (this._file, this._globalOptions,
							this._opOptions)
					: packets.wrq.serialize (this._file);
		}
	}catch (error){
		return this._close (new Error (error.message));
	}
	
	this._sendAndRetransmit (buffer);
};

ClientRequest.prototype._emitDefaultExtensions = function (){
	this._extensionsEmitted = true;
	var stats = {
		blockSize: 512,
		windowSize: 1,
		size: this._opOptions.size || null,
		userExtensions: {}
	};
	this._setStats (stats);
	this._oackExpected = false;
	this._onReady (stats, this._globalOptions.extensions.rollover);
};

ClientRequest.prototype._setStats = function (stats){
	//Save max data length
	this._maxDataLength += stats.blockSize;
	this._blksize = stats.blockSize;
	
	stats.retries = this._retries;
	stats.timeout = this._timeout;
	var address = this._socket.address ();
	stats.localAddress = address.address;
	stats.localPort = address.port;
	stats.remoteAddress = this._address;
	stats.remotePort = this._port;
};

ClientRequest.prototype._onMessage = function (buffer){
	var op = buffer.readUInt16BE (0);
	
	if (op === opcodes.DATA){
		if (this._isRRQ){
			if (!this._extensionsEmitted) this._emitDefaultExtensions ();
			if (buffer.length < 4 || buffer.length > this._maxDataLength){
				return this._sendErrorAndClose (errors.EBADMSG);
			}
			try{
				this._onData (packets.data.deserialize (buffer, this._blksize));
			}catch (error){
				this._sendErrorAndClose (error);
			}
		}else{
			this._sendErrorAndClose (errors.EBADOP);
		}
	}else if (op === opcodes.ACK){
		if (!this._isRRQ){
			if (!this._extensionsEmitted){
				//The server doesn't return extensions, it's probably that it doesn't
				//rollover automatically (old server), so we can abort the transfer
				//prematurely
				//Check the size (65535x512)
				if (this._opOptions.size > 33553920){
					return this._sendErrorAndClose (errors.EFBIG);
				}
				this._emitDefaultExtensions ();
				//The first ACK with block 0 is ignored
				if (buffer.length !== 4 || buffer.readUInt16BE (2) !== 0){
					this._sendErrorAndClose (errors.EBADMSG);
				}
			}else{
				try{
					this._onAck (packets.ack.deserialize (buffer));
				}catch (error){
					this._sendErrorAndClose (error);
				}
			}
		}else{
			this._sendErrorAndClose (errors.EBADOP);
		}
	}else if (op === opcodes.OACK){
		//OACK can be only received when RRQ/WRQ is sent
		if (!this._oackExpected) return this._sendErrorAndClose (errors.EBADOP);
		this._oackExpected = false;
		try{
			this._onOackMessage (packets.oack.deserialize (buffer));
		}catch (error){
			this._sendErrorAndClose (error);
		}
	}else if (op === opcodes.ERROR){
		if (buffer.length < 4) return this._closeWithError (errors.EBADMSG);
		try{
			this._onErrorMessage (packets.error.deserialize (buffer));
		}catch (error){
			this._closeWithError (error);
		}
	}else{
		this._sendErrorAndClose (errors.EBADOP);
	}
};

ClientRequest.prototype._onOackMessage = function (message){
	var userExtensions = {};
	
	for (var p in message){
		//Fail if the OACK message contains invalid extensions
		if (!knownExtensions[p]){
			if (!(p in this._opOptions.userExtensions)){
				return this._sendErrorAndClose (errors.EDENY);
			}else{
				userExtensions[p] = message[p];
			}
		}
	}
	
	var blockSize;
	var transferSize;
	var windowSize;
	var rollover;
	
	if (message.timeout){
		var timeout = ~~message.timeout;
		if (timeout > 0 && timeout <= this._timeout){
			this._timeout = timeout;
		}else{
			return this._sendErrorAndClose (errors.EDENY);
		}
	}
	
	if (message.blksize){
		blockSize = ~~message.blksize;
		if (blockSize < 8 || blockSize > this._globalOptions.extensions.blksize){
			return this._sendErrorAndClose (errors.EDENY);
		}
	}
	
	if (message.tsize){
		transferSize = ~~message.tsize;
		if (transferSize < 0 ||
				(this._opOptions.size !== undefined &&
						transferSize !== this._opOptions.size)){
			return this._sendErrorAndClose (errors.EDENY);
		}
	}
	
	if (message.windowsize){
		windowSize = ~~message.windowsize;
		if (windowSize <= 0 ||
				windowSize > this._globalOptions.extensions.windowsize){
			return this._sendErrorAndClose (errors.EDENY);
		}
	}
	
	if (message.rollover){
		rollover = ~~message.rollover;
		if (rollover < 0 || rollover > 1){
			return this._sendErrorAndClose (errors.EDENY);
		}
	}
	
	this._extensionsEmitted = true;
	rollover = rollover !== undefined
				? rollover
				: this._globalOptions.extensions.rollover;
	var stats = {
		blockSize: blockSize || 512,
		windowSize: windowSize || 1,
		size: transferSize !== undefined ? transferSize : null,
		userExtensions: userExtensions
	};
	this._setStats (stats);
	
	if (this._isRRQ){
		//Acknowledge OACK
		//The ACK of the block 0 is retransmitted from the reader
		this._sendAck (0);
		this._onReady (stats, rollover, true);
	}else{
		this._onReady (stats, rollover);
	}
};

ClientRequest.prototype._onErrorMessage = function (message){
	if (this._oackExpected && message.code === 8){
		if (this._extensionsRetransmitted){
			//The server has returned an ERROR with code 8 after a RRQ/WRQ without
			//extensions. The code 8 is only used when RRQ and WRQ messages contain
			//extensions
			return this._closeWithError (errors.EBADOP);
		}
		
		//If the error code is 8, the server doesn't like one or more extensions
		//Retransmit without extensions
		this._extensionsRetransmitted = true;
		this._port = this._globalOptions.port;
		this._firstPacket = this._first = true;
		
		//In order to retransmit, the socket must be closed and open a new one, that
		//is, cannot reuse the same socket because the server closes its socket
		this._socket.removeListener ("close", this._onCloseFn);
		var me = this;
		this._socket.on ("close", function (){
			me._open ();
		});
		this._socket.close ();
	}else{
		this._closeWithError (message);
	}
};