/*
 * This program is free software: you can redistribute it and/or modify it under
 * the terms of the GNU General Public License as published by the Free Software
 * Foundation, either version 3 of the License, or (at your option) any later
 * version.
 * 
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
 * details.
 * 
 * You should have received a copy of the GNU General Public License along with
 * this program. If not, see <http://www.gnu.org/licenses/>.
 */
package com.l2jserver.communityserver.network.netcon;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.net.Socket;
import java.net.UnknownHostException;

import com.l2jserver.communityserver.network.netcon.crypt.NewCrypt;

/**
 * @author Forsaiken
 */
public abstract class NetConnection extends Thread
{
	private static final NewCrypt INITIAL_CRYPT = new NewCrypt("_;v.]05-31!|+-%xT!^[$\00");
	
	private final NetConnectionConfig _config;
	
	/* TCP */
	private Socket _tcpCon;
	private BufferedInputStream _tcpIn;
	private BufferedOutputStream _tcpOut;
	
	/* CRYPT */
	private NewCrypt _crypt;
	
	protected NetConnection(NetConnectionConfig config)
	{
		_config = config;
	}
	
	public final void connect(final String address, final int port) throws UnknownHostException, IOException
	{
		connect(new Socket(address, port));
	}
	
	public final void connect(final Socket remoteConnection) throws IOException
	{
		if (isConnected())
			throw new IOException("TCP Connect: Allready connected.");
		
		_crypt = INITIAL_CRYPT;
		
		_tcpCon = remoteConnection;
		_tcpOut = new BufferedOutputStream(_tcpCon.getOutputStream(), _config.TCP_SEND_BUFFER_SIZE);
		_tcpIn = new BufferedInputStream(_tcpCon.getInputStream(), _config.TCP_RECEIVE_BUFFER_SIZE);
	}
	
	public final boolean isConnected() throws IOException
	{
		return _tcpCon != null && _tcpCon.isConnected();
	}
	
	public final int getConnectionPort() throws IOException
	{
		if (!isConnected())
			throw new IOException("TCP: Not connected.");
		
		return _tcpCon.getPort();
	}
	
	public final String getConnectionAddress() throws IOException
	{
		if (!isConnected())
			throw new IOException("TCP: Not connected.");
			
		return _tcpCon.getInetAddress().getHostAddress();
	}
	
	protected final byte[] read() throws IOException
	{
		if (_tcpCon == null)
			throw new IOException("TCP Read: Not initialized.");
		
		if (_tcpCon.isClosed())
			throw new IOException("TCP Read: Connection closed.");
		
		final int lengthLo = _tcpIn.read();
		final int lengthHi = _tcpIn.read();
		final int length = lengthHi * 256 + lengthLo;
		
		if (lengthHi < 0)
			throw new IOException("TCP Read: Failed reading.");
		
		final byte[] data = new byte[length - 2];
		
		int receivedBytes = 0;
		int newBytes = 0;
		int left = length - 2;
		while ((newBytes != -1) && (receivedBytes < length - 2))
		{
			if (receivedBytes == 0)
				newBytes = this._tcpIn.read(data, 0, left);
			else
				newBytes = this._tcpIn.read(data, receivedBytes, left);
			receivedBytes += newBytes;
			left -= newBytes;
		}
		
		if (receivedBytes != length - 2)
			throw new IOException("TCP Read: Incomplete Packet recived.");
		
		return decrypt(data);
	}
	
	protected final void write(final BaseWritePacket packet) throws IOException
	{
		if (_tcpCon == null)
			throw new IOException("TCP Write: Not initialized.");
		
		if (_tcpCon.isClosed())
			throw new IOException("TCP Write: Connection closed.");
		
		final byte[] data = crypt(packet.getContent());
		final int len = data.length + 2;
		
		synchronized (_tcpOut)
		{
			_tcpOut.write(len & 0xFF);
			_tcpOut.write(len >> 8 & 0xFF);
			_tcpOut.write(data);
			_tcpOut.flush();
		}
	}
	
	protected final void close(final BaseWritePacket packet) throws IOException
	{
		try
		{
			if (packet != null)
				write(packet);
		}
		finally
		{
			if (_tcpIn != null)
			{
				_tcpIn.close();
				_tcpIn = null;
			}
			
			if (_tcpOut != null)
			{
				_tcpOut.close();
				_tcpOut = null;
			}
			
			if (_tcpCon != null)
			{
				_tcpCon.close();
				_tcpCon = null;
			}
		}
	}
	
	public final void setCrypt(final NewCrypt crypt)
	{
		_crypt = crypt;
	}
	
	private final byte[] decrypt(byte[] data) throws IOException
	{
		data = _crypt.decrypt(data);
		
		if (!NewCrypt.verifyChecksum(data))
			throw new IOException("CRYPT: Incorrect packet checksum.");
		
		return data;
	}
	
	private final byte[] crypt(final byte[] data) throws IOException
	{
		NewCrypt.appendChecksum(data);
		
		return _crypt.crypt(data);
	}
	
	public abstract void run();
}