/**************************************************************
 * 
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 * 
 *************************************************************/



package org.openoffice.test.vcl.client;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.List;
import java.util.Vector;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.openoffice.test.common.SystemUtil;

/**
 * Manage the communication with the automation server.
 * It's used to establish the connection, send and receive data package.
 * Data package format:
 * 
 * | [Force Multi Channel (0xFFFFFFFF)] | Data Length (32 bits) | Check Byte (8 bits) | Header Length (16 bits) | Header Data | Body Data |
 * 
 * To handle the received data, add a communication listener to the manager.
 * The listner will be called back when data package arrives.
 *
 */
public class CommunicationManager implements Runnable, Constant{
	
	private static Logger logger = Logger.getLogger("CommunicationManager");
	
	private final static int DEFAULT_PORT = 12479;
	
	private String host = "localhost";

	private int port = DEFAULT_PORT;

	private Socket socket = null;
	
	private double reconnectInterval = 2;
	
	private int reconnectCount = 5;
	
	private List<CommunicationListener> listeners = new Vector<CommunicationListener>();
	
	/**
	 * Create a communication manager with the default host and port.
	 * The default host is local and the default port is 12479.
	 *
	 */
	public CommunicationManager() {
		try {
			String portValue = System.getProperty("openoffice.automation.port");
			if (portValue != null)
				port = Integer.parseInt(portValue);
		} catch (NumberFormatException e) {
			// use default
		}
	}
	
	/**
	 * Create a communication manager with the given host and port
	 * @param host
	 * @param port
	 */
	public CommunicationManager(String host, int port) {
		this.host = host;
		this.port = port;
	}
	
	/**
	 * Send a data package to server
	 * @param headerType the package header type
	 * @param header the data in the header
	 * @param data the data in the body
	 */
	public synchronized void sendPackage(int headerType, byte[] header, byte[] data) throws CommunicationException {
		if (socket == null)
			start();
		
		try {
			if (header == null)
				header = new byte[0];
			
			if (data == null)
				data = new byte[0];
			
			DataOutputStream os = new DataOutputStream(socket.getOutputStream());
			int len = 1 + 2 + 2 + header.length + data.length;
			// Total len
			os.writeInt(len);
			// Check byte
			os.writeByte(calcCheckByte(len));
			// Header len
			os.writeChar(2 + header.length);
			// Header
			os.writeChar(headerType);
			os.write(header);
			// Data
			os.write(data);
			os.flush();
		} catch (IOException e) {
			stop();
			throw new CommunicationException("Failed to send data to automation server!", e);
		}
	}
	
	/**
	 * Start a new thread to read the data sent by sever
	 */
	public void run() {
		try {
			while (socket != null) {
				DataInputStream is = new DataInputStream(socket.getInputStream());
				
				int len = is.readInt();
				if (len == 0xFFFFFFFF) 
					len = is.readInt();

				byte checkByte = is.readByte();
				if (calcCheckByte(len) != checkByte)
					throw new CommunicationException("Bad data package. Wrong check byte.");

				int headerLen = is.readUnsignedShort();
				int headerType = is.readUnsignedShort();
				byte[] header = new byte[headerLen - 2];
				is.readFully(header);
				byte[] data = new byte[len - headerLen - 3];
				is.readFully(data);
				for (int i = 0; i < listeners.size(); i++)
					((CommunicationListener) listeners.get(i)).received(headerType, header, data);
			}
		} catch (Exception e) {
			logger.log(Level.FINEST, "Failed to receive data!", e);
			stop();
		}
	}
	
	/**
	 * Add a communication listener
	 * @param listener
	 */
	public void addListener(CommunicationListener listener) {
		if (listener != null && !listeners.contains(listener))
			listeners.add(listener);
	}
	
	
	/**
	 * Stop the communication manager.
	 *
	 */
	public synchronized void stop() {
		if (socket == null)
			return;
			
		try {
			socket.close();
		} catch (IOException e) {
			//ignore
		}
		socket = null;
		logger.log(Level.CONFIG, "Stop Communication Manager");
		for (int i = 0; i < listeners.size(); i++)
			((CommunicationListener) listeners.get(i)).stop();
	}
	
	public synchronized boolean isConnected() {
		return socket != null;
	}

	
	public synchronized void connect() throws IOException {
		if (socket != null)
			return;
		
		try{
			socket = new Socket();
			socket.setTcpNoDelay(true);
			socket.setSoTimeout(240 * 1000); // if in 4 minutes we get nothing from server, an exception will thrown.
			socket.connect(new InetSocketAddress(host, port));
			Thread thread = new Thread(this);
			thread.setDaemon(true);
			thread.start();
		} catch (IOException e){
			socket = null;
			throw e;
		}
	}
	
	/**
	 * Start the communication manager.
	 * 
	 */
	public synchronized void start() {
		logger.log(Level.CONFIG, "Start Communication Manager");
		//connect and retry if fails
		for (int i = 0; i < reconnectCount; i++) {
			try {
				connect();
				return;
			} catch (IOException e) {
				logger.log(Level.FINEST, "Failed to connect! Tried " + i, e);
			}
			
			SystemUtil.sleep(reconnectInterval);
		}
		
		throw new CommunicationException("Failed to connect to automation server on: " + host + ":" + port);
	}
	
	
	private static byte calcCheckByte(int i) {
		int nRes = 0;
		int[] bytes = new int[4];
		bytes[0] = (i >>> 24) & 0x00FF;
		bytes[1] = (i >>> 16) & 0x00FF;
		bytes[2] = (i >>> 8) & 0x00FF;
		bytes[3] = (i >>> 0) & 0x00FF;
		nRes += bytes[0] ^ 0xf0;
		nRes += bytes[1] ^ 0x0f;
		nRes += bytes[2] ^ 0xf0;
		nRes += bytes[3] ^ 0x0f;
		nRes ^= (nRes >>> 8);
		return (byte) nRes;
	}
}
