/*
* Copyright (c) 2010 Nokia Corporation and/or its subsidiary(-ies).
* All rights reserved.
* This component and the accompanying materials are made available
* under the terms of the License "Eclipse Public License v1.0"
* which accompanies this distribution, and is available
* at the URL "http://www.eclipse.org/legal/epl-v10.html".
*
* Initial Contributors:
* Nokia Corporation - initial contribution.
*
* Contributors:
*
* Description: 
*
*/
package com.nokia.carbide.remoteconnections.discovery.pccs.pccsnative;

import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedList;

import org.eclipse.cdt.utils.WindowsRegistry;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.swt.widgets.Display;

import com.nokia.carbide.remoteconnections.discovery.pccs.Activator;
import com.nokia.carbide.remoteconnections.discovery.pccs.Messages;
import com.nokia.carbide.remoteconnections.discovery.pccs.pccsnative.IConnAPILibrary.IConnAPIDeviceCallback;
import com.sun.jna.ptr.IntByReference;
import com.sun.jna.ptr.ShortByReference;

public class PCCSConnection {

	private boolean DEBUG = false;
	
	/**
	 * @author chpeckha
	 *
	 */
	public class DeviceNotificationCallback implements IConnAPIDeviceCallback {

		/* (non-Javadoc)
		 * @see com.nokia.carbide.remoteconnections.discovery.pccs.pccsnative.IConnAPILibrary.IConnAPIDeviceCallback#invoke(int, com.sun.jna.ptr.ShortByReference)
		 */
		public int invoke(final int dwStatus, final ShortByReference pstrSerialNumber) {
			Display.getDefault().asyncExec(
				new Runnable ()	{
					public void run() {
						// check for NULL, since I am seeing bogus events coming from PCCS before the real one
						//  and the serial number is not filled in. If the serial number is null,
						//  everything else will be null (according to the PCCS docs)
						//  TODO: bug in PCCS API: 
						if (DEBUG) System.out.printf("DeviceNotificationCallback %x %s\n", dwStatus, (pstrSerialNumber == null ? "serNum: null" : pstrSerialNumber.getPointer().getString(0, true))); //$NON-NLS-1$ //$NON-NLS-2$
						String serialNumber = "unknown device"; //$NON-NLS-1$
						if (pstrSerialNumber != null) {
							serialNumber = pstrSerialNumber.getPointer().getString(0, true);
						}
						if (DEBUG) System.out.printf("DeviceNotificationCallback %x %s\n", dwStatus, serialNumber); //$NON-NLS-1$

						DeviceEventListener.DeviceEvent eventType = DeviceEventListener.DeviceEvent.DEVICE_LIST_UPDATED;
						// decode dwStatus per PCCS docs
						switch(DMAPIDefinitions.GET_CONAPI_CB_STATUS(dwStatus)) {
						case DMAPIDefinitions.CONAPI_DEVICE_LIST_UPDATED:
							eventType = DeviceEventListener.DeviceEvent.DEVICE_LIST_UPDATED;
							break;
						case DMAPIDefinitions.CONAPI_DEVICE_ADDED:
							eventType = DeviceEventListener.DeviceEvent.DEVICE_ADDED;
							break;
						case DMAPIDefinitions.CONAPI_DEVICE_REMOVED:
							eventType = DeviceEventListener.DeviceEvent.DEVICE_REMOVED;
							break;
						case DMAPIDefinitions.CONAPI_DEVICE_UPDATED:
							switch(DMAPIDefinitions.GET_CONAPI_CB_INFO(dwStatus)) {
							case DMAPIDefinitions.CONAPI_CONNECTION_ADDED:
								eventType = DeviceEventListener.DeviceEvent.DEVICE_UPDATED_ADDEDCONNECTION;
								break;
							case DMAPIDefinitions.CONAPI_CONNECTION_REMOVED:
								eventType = DeviceEventListener.DeviceEvent.DEVICE_UPDATED_REMOVEDCONNECTION;
								break;
							case DMAPIDefinitions.CONAPI_DEVICE_RENAMED:
								eventType = DeviceEventListener.DeviceEvent.DEVICE_UPDATED_RENAMED;
								break;
							}
						}
						// fire events
						Iterator<DeviceEventListener> iter = listeners.iterator();
						while (iter.hasNext()) {
							iter.next().onDeviceEvent(eventType, serialNumber);
						}
					}
				}
			);
			return PCCSErrors.CONA_OK;
		}
	}

	private static final Collection<DeviceEventListener> listeners = new LinkedList<DeviceEventListener>();
	private IConnAPILibrary library;
	private LPDMHANDLE dmHandle = new LPDMHANDLE();
	private DeviceNotificationCallback pfnCallback = new DeviceNotificationCallback();
	public static final int PCCS_NOT_FOUND = 1;
	public static final int PCCS_WRONG_VERSION = 2;
	
	private LPMCHANDLE mcHandle = new LPMCHANDLE();
	
	/**
	 * 
	 */
	public PCCSConnection() {
	}

	public void open() throws CoreException {
		if (library == null) {
			library = ConnAPILibrary.getInstance();
		}
		loadDMAPI();
//		loadMCAPI(); TODO: not tested yet
	}
	
	/**
	 * Initializes the Device Management API (DMAPI) for use.
	 * 
	 * @throws CoreException
	 */
	private void loadDMAPI() throws CoreException {

		int dwResult = library.DMAPI_Initialize(DMAPIDefinitions.DMAPI_VERSION_34, null);
    	if (dwResult != PCCSErrors.CONA_OK) {
    		library.DMAPI_Terminate(null);
    		String msg;
    		if (Activator.isSymSEELayout()) {
        		msg = String.format("PCCS DMAPI_Initialize API returned error on initialization %x", dwResult); //$NON-NLS-1$
        		if (dwResult == PCCSErrors.ECONA_UNSUPPORTED_API_VERSION) {
        			msg = Messages.PCCSConnection_PCCS_Version_Error + Activator.getLoadErrorURL();
        		}
    		} else {
    			msg = Messages.PCCSConnection_PCSuite_Version_Error + Activator.getLoadErrorURL();
    		}
    		throw new CoreException(new Status(IStatus.ERROR, Activator.PLUGIN_ID, PCCS_WRONG_VERSION, msg, null));
    	}
    	
    	// initialize common API
//    	dwResult = library.CFAPI_Initialize(DMAPIDefinitions.CFAPI_VERSION_10, null); unnecessary
//    	if (dwResult != PCCSErrors.CONA_OK) {
//    		library.DMAPI_Terminate(null);
//    		String msg = String.format("PCCS CFAPI_Initialize API returned error on initialization %x", dwResult);
//    		throw new CoreException(new Status(IStatus.ERROR, Activator.PLUGIN_ID, dwResult, msg, null));
//    	}
    	
    	// open a DM handle
    	dmHandle.setValue(DMHANDLE.INVALID_HANDLE_VALUE);
    	dwResult = library.CONAOpenDM(dmHandle);
    	if (dwResult != PCCSErrors.CONA_OK) {
    		library.DMAPI_Terminate(null);
    		String msg = String.format(Messages.PCCSConnection_PCCS_CONAOpenDM_Error, dwResult);
    		if (dwResult == PCCSErrors.ECONA_NOT_ENOUGH_MEMORY) {
    			msg = Messages.PCCSConnection_PCCS_Not_Enough_Memory_Error;
    		}
    		throw new CoreException(new Status(IStatus.ERROR, Activator.PLUGIN_ID, dwResult, msg, null));
    	}
    	
    	// register a call back
    	dwResult = library.CONARegisterNotifyCallback(dmHandle.getValue(), PCCSTypeDefinitions.API_REGISTER, pfnCallback);
    	if (dwResult != PCCSErrors.CONA_OK) {
//    		System.out.printf("CONAOpenDM returned: %x\n", dwResult);
    		library.DMAPI_Terminate(null);
    		library.CONACloseDM(dmHandle.getValue());
        	dmHandle.setValue(DMHANDLE.INVALID_HANDLE_VALUE);

        	String msg = String.format(Messages.PCCSConnection_PCCS_CONARegisterNotifyCallback_Error, dwResult);
    		if (dwResult == PCCSErrors.ECONA_INVALID_POINTER) {
    			msg = Messages.PCCSConnection_PCCS_CONARegisterNotifyCallback_Pointer_Error;
    		}
    		throw new CoreException(new Status(IStatus.ERROR, Activator.PLUGIN_ID, dwResult, msg, null));
    	}
	}

	private void loadMCAPI() throws CoreException {
		int dwResult = PCCSErrors.CONA_OK;
		dwResult = library.MCAPI_Initialize(MCAPIDefinitions.MCAPI_VERSION_32, null);
    	if (dwResult != PCCSErrors.CONA_OK) {
    		library.MCAPI_Terminate(null);
    		String msg;
    		if (Activator.isSymSEELayout()) {
        		msg = String.format("PCCS MCAPI_Initialize API returned error on initialization %x", dwResult); //$NON-NLS-1$
        		if (dwResult == PCCSErrors.ECONA_UNSUPPORTED_API_VERSION) {
        			msg = Messages.PCCSConnection_PCCS_Version_Error + Activator.getLoadErrorURL();
        		}
    		} else {
    			msg = Messages.PCCSConnection_PCSuite_Version_Error + Activator.getLoadErrorURL();
    		}
    		throw new CoreException(new Status(IStatus.ERROR, Activator.PLUGIN_ID, PCCS_WRONG_VERSION, msg, null));
    	}
    	dwResult = library.CONAOpenMM(mcHandle, 0);
   
    	if (dwResult != PCCSErrors.CONA_OK) {
    		library.MCAPI_Terminate(null);
    		String msg = String.format("PCCS CONAOpenMM API returned error on initialization %x", dwResult);
    		if (dwResult == PCCSErrors.ECONA_NOT_ENOUGH_MEMORY) {
    			msg = Messages.PCCSConnection_PCCS_Not_Enough_Memory_Error;
    		}
    		throw new CoreException(new Status(IStatus.ERROR, Activator.PLUGIN_ID, dwResult, msg, null));
    	}
	}

	public void close() throws CoreException {
		if (library == null)
			return;
		
//		closeMCAPI(); TODO: not tested yet
		closeDMAPI();
	}
	
	/**
	 * Closes the DMAPI.
	 * @throws CoreException
	 */
	private void closeDMAPI() throws CoreException {
		int dwResult = PCCSErrors.CONA_OK;
		if (dmHandle.getValue() != DMHANDLE.INVALID_HANDLE_VALUE) {
			// unregister callback
			dwResult = library.CONARegisterNotifyCallback(dmHandle.getValue(), PCCSTypeDefinitions.API_UNREGISTER, pfnCallback);
			// close DM connection
			dwResult = library.CONACloseDM(dmHandle.getValue());
        	dmHandle.setValue(DMHANDLE.INVALID_HANDLE_VALUE);
        	// Terminate Common Functions API
//        	dwResult = library.CFAPI_Terminate(null); unnecessary
        	// Terminate Device management API
        	dwResult = library.DMAPI_Terminate(null);
		}
	}

	private void closeMCAPI() {
		int dwResult = PCCSErrors.CONA_OK;
		if (mcHandle.getValue() != MCHANDLE.INVALID_HANDLE_VALUE) {
			// close DM connection
			dwResult = library.CONACloseDM(mcHandle.getValue());
			mcHandle.setValue(MCHANDLE.INVALID_HANDLE_VALUE);
        	// Terminate Device management API
    		dwResult = library.MCAPI_Terminate(null);
		}
	}
	/**
	 * @return null if no device found.
	 * @throws CoreException
	 */
	public DeviceInfo[] getDeviceList() throws CoreException {
		
		DeviceInfo[] deviceInfo = null;
		
		int dwResult = PCCSErrors.CONA_OK;
		IntByReference pdwCount = new IntByReference(0);
		dwResult = library.CONAGetDeviceCount(dmHandle.getValue(), pdwCount);
		if (DEBUG) System.out.printf("CONAGetDeviceCount: %x number of devices: %d\n", dwResult, pdwCount.getValue()); //$NON-NLS-1$

		int deviceCnt = pdwCount.getValue();
		if (dwResult != PCCSErrors.CONA_OK || deviceCnt == 0)
			return deviceInfo;
		
		// array of structs in contiguous memory required !
		CONAPI_DEVICE[] pDevices = (CONAPI_DEVICE[])new CONAPI_DEVICE().toArray(deviceCnt);
		dwResult = library.CONAGetDevices(dmHandle.getValue(), pdwCount, pDevices);
		if (DEBUG) System.out.printf("CONAGetDevices: %x number of devices: %d\n", dwResult, deviceCnt); //$NON-NLS-1$
		
		if (dwResult == PCCSErrors.CONA_OK) {
			// extract to regular java types
			if (deviceCnt > 0) {
				deviceInfo = new DeviceInfo[deviceCnt];
				CONAPI_DEVICE[] devices = pDevices;
				for (int i = 0; i < deviceCnt; i++) {
					deviceInfo[i] = new DeviceInfo();
					if (devices[i].pstrSerialNumber != null) {
						deviceInfo[i].serialNumber = devices[i].pstrSerialNumber.getPointer().getString(0, true);
					} else {
						// TODO: docs say if ser num == null --> device unsupported, but this is a bug in PCCS
						deviceInfo[i].serialNumber = "not known"; //$NON-NLS-1$
					}
					if (devices[i].pstrFriendlyName != null) {
						deviceInfo[i].friendlyName = devices[i].pstrFriendlyName.getPointer().getString(0, true);
					} else {
						deviceInfo[i].friendlyName = "not known"; //$NON-NLS-1$
					}
					if (devices[i].pstrModel != null) {
						deviceInfo[i].model = devices[i].pstrModel.getPointer().getString(0, true);
					} else {
						deviceInfo[i].model = "not known"; //$NON-NLS-1$
					}
					if (devices[i].pstrManufacturer != null) {
						deviceInfo[i].mfr = devices[i].pstrManufacturer.getPointer().getString(0, true);
					} else {
						deviceInfo[i].mfr = "not known"; //$NON-NLS-1$
					}
					int numItems = deviceInfo[i].numberOfConnections = devices[i].dwNumberOfItems;
					
					CONAPI_CONNECTION_INFO[] conn = null;
					if (numItems > 0)
						 conn = (CONAPI_CONNECTION_INFO[])devices[i].pItems.toArray(numItems);
					for (int j = 0; j < numItems; j++) {
						DeviceConnectionInfo connInfo = new DeviceConnectionInfo();
						connInfo.deviceID = conn[j].dwDeviceID;
						switch(conn[j].dwMedia){
						case PCCSTypeDefinitions.API_MEDIA_BLUETOOTH:
							connInfo.media = "bluetooth"; //$NON-NLS-1$
							break;
						case PCCSTypeDefinitions.API_MEDIA_IRDA:
							connInfo.media = "irda"; //$NON-NLS-1$
							break;
						case PCCSTypeDefinitions.API_MEDIA_SERIAL:
							connInfo.media = "serial-ca42"; //$NON-NLS-1$
							break;
						default:
						case PCCSTypeDefinitions.API_MEDIA_USB:
							connInfo.media = "usb"; //$NON-NLS-1$
							break;
						}
						if (conn[j].pstrDeviceName != null) {
							connInfo.deviceName = conn[j].pstrDeviceName.getPointer().getString(0,true);
							if (deviceInfo[i].friendlyName.equals("not known")) { //$NON-NLS-1$
								// use device name as friendly name (latter was null)
								deviceInfo[i].friendlyName = connInfo.deviceName;
							}
						} else {
							connInfo.deviceName = "not known"; //$NON-NLS-1$
						}
						// Bomb if friendly name is not known or address is not known
						//   since these are essential to a connection
						if (conn[j].pstrAddress == null || deviceInfo[i].friendlyName.equals("not known")) //$NON-NLS-1$
							continue;
						
						connInfo.address = conn[j].pstrAddress.getPointer().getString(0, true);
						connInfo.state = conn[j].dwState;
						
						if (connInfo.media.equals("usb")) { //$NON-NLS-1$
							if (isGoodUSBState(connInfo.state)){ 
								connInfo.comPort = getUSBComPort(connInfo.address);
							} else {
								// don't store - not in good USB personality mode
								continue;
							}
						} else if (connInfo.media.equals("bluetooth")) { //$NON-NLS-1$
							// TODO: implement BT
							connInfo.comPort = getBTComPort(connInfo.address);
						} else {
							// IRDA and Serial(CA42) probably will not come here anyway
							connInfo.comPort = null;
						}
						if (connInfo.comPort != null)
							deviceInfo[i].connections.add(connInfo);
					}
				}
			}
			dwResult = library.CONAFreeDeviceStructure(deviceCnt, pDevices);
			if (DEBUG) System.out.printf("CONAFreeDeviceStructure: %x\n", dwResult); //$NON-NLS-1$
		}
		return deviceInfo;
	}

	private boolean isGoodUSBState(int state) {
		// This test is necessary since we're seeing
		// on some devices PCSuite mode not set BUT PCSuite trusted flag is set
		// OR PCSuite mode on device is selected BUT only Paired flag is set
		// good if:
		// in PCSuite mode
		// OR is Paired
		// OR is PCSuite trusted
		// TODO: use UPAPI here instead
		if (DMAPIDefinitions.CONAPI_IS_IN_PCSUITE_MODE(state))
			return true;
		else if (DMAPIDefinitions.CONAPI_IS_DEVICE_PAIRED(state))
			return true;
		else if (DMAPIDefinitions.CONAPI_IS_PCSUITE_TRUSTED(state))
			return true;
		return false;
	}

	private String getBTComPort(String address) {
		// TODO Auto-generated method stub
		return null;
	}

	public String getUSBComPort(String address) {
		if (address != null && address.length() > 0) {
			String id, key, subKey, parentIdPrefix = null, portName;

			WindowsRegistry wr = WindowsRegistry.getRegistry();
			key = "SYSTEM\\ControlSet001\\Enum\\USB" + address.substring(address.indexOf('\\'), address.lastIndexOf('\\')); //$NON-NLS-1$
			id = address.substring(address.lastIndexOf('\\') + 1);
			int i = 0;
			do {
				subKey = wr.getLocalMachineKeyName(key, i);
				if (subKey != null && subKey.equalsIgnoreCase(id)) {
					key += "\\" + subKey; //$NON-NLS-1$
					parentIdPrefix = wr.getLocalMachineValue(key, "ParentIdPrefix"); //$NON-NLS-1$
					break;
				}
				i++;
			} while (subKey != null);
			if (parentIdPrefix != null) {
				key = "SYSTEM\\ControlSet001\\Enum\\NMWCD"; //$NON-NLS-1$
				subKey = address.substring(address.indexOf('\\'), address.lastIndexOf('\\'));
				key += subKey + "&IF_JAVACOMM"; //$NON-NLS-1$
				i = 0;
				do {
					subKey = wr.getLocalMachineKeyName(key, i);
					if (subKey != null && subKey.contains(parentIdPrefix)) {
						key += "\\" + subKey + "\\Device Parameters"; //$NON-NLS-1$ //$NON-NLS-2$
						portName = wr.getLocalMachineValue(key, "PortName"); //$NON-NLS-1$
						if (portName != null) {
							portName = portName.substring(portName.indexOf('M') + 1);
							return portName;
						}
						break;
					}
					i++;
				} while (subKey != null);
			}
		}
		return null;
	}

	public void addEventListenter(DeviceEventListener listener) {
		listeners.add(listener);
	}

	public void removeEventListener(DeviceEventListener listener) {
		listeners.remove(listener);
	}

	public interface DeviceEventListener {
		public enum DeviceEvent {
			DEVICE_LIST_UPDATED,		// not any specific information 
			DEVICE_ADDED, 
			DEVICE_REMOVED, 
			DEVICE_UPDATED_ADDEDCONNECTION,
			DEVICE_UPDATED_REMOVEDCONNECTION,
			DEVICE_UPDATED_RENAMED, 
			DEVICE_UPDATED_UNKNOWN,
		}
		public void onDeviceEvent(DeviceEvent eventType, String serialNumber);
	}
}
