/* Copyright (c) 2011 Timothy Wall, All Rights Reserved
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library 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
 * Lesser General Public License for more details.
 */
package com.sun.jna.platform.win32;

import java.util.HashMap;
import java.util.Map;

import com.sun.jna.platform.win32.WinDef.BOOLByReference;
import com.sun.jna.platform.win32.WinNT.HANDLE;
import com.sun.jna.platform.win32.WinNT.HANDLEByReference;
import com.sun.jna.platform.win32.WinRas.RASCONN;
import com.sun.jna.platform.win32.WinRas.RASCREDENTIALS;
import com.sun.jna.platform.win32.WinRas.RASDIALPARAMS;
import com.sun.jna.platform.win32.WinRas.RASENTRY;
import com.sun.jna.platform.win32.WinRas.RASPPPIP;
import com.sun.jna.platform.win32.WinRas.RasDialFunc2;
import com.sun.jna.ptr.IntByReference;

/**
 * Rasapi32 utility API.
 */
@SuppressWarnings("unchecked")
public abstract class Rasapi32Util {
	private static final int RASP_PppIp = 0x8021;

	private static Object phoneBookMutex = new Object();
	@SuppressWarnings("rawtypes")
	public static final Map CONNECTION_STATE_TEXT = new HashMap();

	static {
		CONNECTION_STATE_TEXT.put(0, "Opening the port...");
		CONNECTION_STATE_TEXT.put(1, "Port has been opened successfully");
		CONNECTION_STATE_TEXT.put(2, "Connecting to the device...");
		CONNECTION_STATE_TEXT.put(3, "The device has connected successfully.");
		CONNECTION_STATE_TEXT.put(4, "All devices in the device chain have successfully connected.");
		CONNECTION_STATE_TEXT.put(5, "Verifying the user name and password...");
		CONNECTION_STATE_TEXT.put(6, "An authentication event has occurred.");
		CONNECTION_STATE_TEXT.put(7, "Requested another validation attempt with a new user.");
		CONNECTION_STATE_TEXT.put(8, "Server has requested a callback number.");
		CONNECTION_STATE_TEXT.put(9, "The client has requested to change the password");
		CONNECTION_STATE_TEXT.put(10, "Registering your computer on the network...");
		CONNECTION_STATE_TEXT.put(11, "The link-speed calculation phase is starting...");
		CONNECTION_STATE_TEXT.put(12, "An authentication request is being acknowledged.");
		CONNECTION_STATE_TEXT.put(13, "Reauthentication (after callback) is starting.");
		CONNECTION_STATE_TEXT.put(14, "The client has successfully completed authentication.");
		CONNECTION_STATE_TEXT.put(15, "The line is about to disconnect for callback.");
		CONNECTION_STATE_TEXT.put(16, "Delaying to give the modem time to reset for callback.");
		CONNECTION_STATE_TEXT.put(17, "Waiting for an incoming call from server.");
		CONNECTION_STATE_TEXT.put(18, "Projection result information is available.");
		CONNECTION_STATE_TEXT.put(19, "User authentication is being initiated or retried.");
		CONNECTION_STATE_TEXT.put(20, "Client has been called back and is about to resume authentication.");
		CONNECTION_STATE_TEXT.put(21, "Logging on to the network...");
		CONNECTION_STATE_TEXT.put(22, "Subentry has been connected");
		CONNECTION_STATE_TEXT.put(23, "Subentry has been disconnected");
		CONNECTION_STATE_TEXT.put(4096, "Terminal state supported by RASPHONE.EXE.");
		CONNECTION_STATE_TEXT.put(4097, "Retry authentication state supported by RASPHONE.EXE.");
		CONNECTION_STATE_TEXT.put(4098, "Callback state supported by RASPHONE.EXE.");
		CONNECTION_STATE_TEXT.put(4099, "Change password state supported by RASPHONE.EXE.");
		CONNECTION_STATE_TEXT.put(4100, "Displaying authentication UI");
		CONNECTION_STATE_TEXT.put(8192, "Connected to remote server successfully");
		CONNECTION_STATE_TEXT.put(8193, "Disconnected");
	}

	/**
	 * Exceptions
	 */
	public static class Ras32Exception extends RuntimeException {

		private static final long serialVersionUID = 1L;

		private final int code;

		/**
		 * Returns the error code of the error.
		 * @return
		 *  Error code.
		 */
		public int getCode() {
			return code;
		}

		/**
		 * New Win32 exception from an error code, usually obtained from GetLastError.
		 * @param code
		 *  Error code.
		 */
		public Ras32Exception(int code) {
			super(getRasErrorString(code));
			this.code = code;
		}
	}

	/**
	 * Get the RAS error description
	 * @param code the error code
	 * @return the RAS description
	 */
	public static String getRasErrorString(int code) {
		char[] msg = new char[1024];
		int err = Rasapi32.INSTANCE.RasGetErrorString(code, msg, msg.length);
		if (err != WinError.ERROR_SUCCESS) return "Unknown error " + code;
		int len = 0;
		for (; len < msg.length; len++) if (msg[len] == 0) break;
		return new String(msg, 0, len);
	}

	/**
	 * Translate the connection status value to text
	 * @param connStatus the connection status
	 * @return the descriptive text
	 */
	public static String getRasConnectionStatusText(int connStatus) {
		if (!CONNECTION_STATE_TEXT.containsKey(connStatus)) return Integer.toString(connStatus);
		return (String)CONNECTION_STATE_TEXT.get(connStatus);
	}

	/**
	 * Return a RAS connection by name
	 * @param connName the connection name
	 * @return the RAS connection structure
	 * @throws Ras32Exception errors
	 */
	public static HANDLE getRasConnection(String connName) throws Ras32Exception {
		// size the array needed
		IntByReference lpcb  = new IntByReference(0);
		IntByReference lpcConnections  = new IntByReference();
		int err = Rasapi32.INSTANCE.RasEnumConnections(null, lpcb, lpcConnections);
		if (err != WinError.ERROR_SUCCESS && err != WinRas.ERROR_BUFFER_TOO_SMALL) throw new Ras32Exception(err);
		if (lpcb.getValue() == 0) return null;

		// get the connections
		RASCONN[] connections = new RASCONN[lpcConnections.getValue()];
		for (int i = 0; i < lpcConnections.getValue(); i++) connections[i] = new RASCONN();
		lpcb  = new IntByReference(connections[0].dwSize * lpcConnections.getValue());
		err = Rasapi32.INSTANCE.RasEnumConnections(connections, lpcb, lpcConnections);
		if (err != WinError.ERROR_SUCCESS) throw new Ras32Exception(err);

		// find connection
		for (int i = 0; i < lpcConnections.getValue(); i++) {
			if (new String(connections[i].szEntryName).equals(connName)) return connections[i].hrasconn;
		}
		return null;
	}

	/**
	 * Hangup a connection by name
	 * @param connName the connection name
	 * @throws Ras32Exception errors
	 */
	public static void hangupRasConnection(String connName) throws Ras32Exception {
		HANDLE hrasConn = getRasConnection(connName);
		if (hrasConn == null) return;
		int err = Rasapi32.INSTANCE.RasHangUp(hrasConn);
		if (err != WinError.ERROR_SUCCESS) throw new Ras32Exception(err);
	}

	/**
	 * Hangup a connection
	 * @param hrasConn the connection
	 * @throws Ras32Exception errors
	 */
	public static void hangupRasConnection(HANDLE hrasConn) throws Ras32Exception {
		if (hrasConn == null) return;
		int err = Rasapi32.INSTANCE.RasHangUp(hrasConn);
		if (err != WinError.ERROR_SUCCESS) throw new Ras32Exception(err);
	}

	/**
	 * Get the connection's IP projection
	 * @param hrasConn the RAS connection handle
	 * @return the IP projection
	 * @throws Ras32Exception errors
	 */
	public static RASPPPIP getIPProjection(HANDLE hrasConn) throws Ras32Exception {
		RASPPPIP pppIpProjection = new RASPPPIP();
		IntByReference lpcb = new IntByReference(pppIpProjection.size());
		pppIpProjection.write();
		int err = Rasapi32.INSTANCE.RasGetProjectionInfo(hrasConn, RASP_PppIp, pppIpProjection.getPointer(), lpcb);
		if (err != WinError.ERROR_SUCCESS) throw new Ras32Exception(err);
		pppIpProjection.read();
		return pppIpProjection;
	}

	/**
	 * Return the phone book entry.
	 * @param entryName the entry name
	 * @return the RAS entry
	 * @throws Ras32Exception any errors
	 */
	public static RASENTRY.ByReference getPhoneBookEntry(String entryName) throws Ras32Exception {
		synchronized (phoneBookMutex) {
			RASENTRY.ByReference rasEntry = new RASENTRY.ByReference();
			IntByReference lpdwEntryInfoSize = new IntByReference(rasEntry.size());
			int err = Rasapi32.INSTANCE.RasGetEntryProperties(null, entryName, rasEntry, lpdwEntryInfoSize, null, null);
			if (err != WinError.ERROR_SUCCESS) throw new Ras32Exception(err);
			return rasEntry;
		}
	}

	/**
	 * Set a phone book entry
	 * @param entryName the phone book entry name
	 * @param rasEntry the entry parameters
	 * @throws Ras32Exception errors
	 */
	public static void setPhoneBookEntry(String entryName, RASENTRY.ByReference rasEntry) throws Ras32Exception {
		synchronized (phoneBookMutex) {
			int err = Rasapi32.INSTANCE.RasSetEntryProperties(null, entryName, rasEntry, rasEntry.size(), null, 0);
			if (err != WinError.ERROR_SUCCESS) throw new Ras32Exception(err);
		}
	}

	/**
	 * get a phone book entry's dialing parameters
	 * @param entryName the phone book entry name
	 * @return the entry's dialing parameters parameters
	 * @throws Ras32Exception errors
	 */
	public static RASDIALPARAMS getPhoneBookDialingParams(String entryName) throws Ras32Exception {
		synchronized (phoneBookMutex) {
			RASDIALPARAMS.ByReference rasDialParams = new RASDIALPARAMS.ByReference();
			System.arraycopy(rasDialParams.szEntryName, 0, entryName.toCharArray(), 0, entryName.length());
			BOOLByReference lpfPassword = new BOOLByReference();
			int err = Rasapi32.INSTANCE.RasGetEntryDialParams(null, rasDialParams, lpfPassword);
			if (err != WinError.ERROR_SUCCESS) throw new Ras32Exception(err);
			return rasDialParams;
		}
	}

	/**
	 * Dial a phone book entry by name (Synchronously)
	 * @param entryName The phone book entry name
         * @return result reference
	 * @throws Ras32Exception errors
	 */
	public static HANDLE dialEntry(String entryName) throws Ras32Exception {
		// get the RAS Credentials
		RASCREDENTIALS.ByReference credentials = new RASCREDENTIALS.ByReference();
		synchronized (phoneBookMutex) {
			credentials.dwMask = WinRas.RASCM_UserName | WinRas.RASCM_Password | WinRas.RASCM_Domain;
			int err = Rasapi32.INSTANCE.RasGetCredentials(null, entryName, credentials);
			if (err != WinError.ERROR_SUCCESS) throw new Ras32Exception(err);
		}

		// set the dialing parameters
		RASDIALPARAMS.ByReference rasDialParams = new RASDIALPARAMS.ByReference();
		System.arraycopy(entryName.toCharArray(), 0, rasDialParams.szEntryName, 0, entryName.length());
		System.arraycopy(credentials.szUserName, 0, rasDialParams.szUserName, 0, credentials.szUserName.length);
		System.arraycopy(credentials.szPassword, 0, rasDialParams.szPassword, 0, credentials.szPassword.length);
		System.arraycopy(credentials.szDomain, 0, rasDialParams.szDomain, 0, credentials.szDomain.length);

		// dial
		HANDLEByReference hrasConn = new HANDLEByReference();
		int err = Rasapi32.INSTANCE.RasDial(null, null, rasDialParams, 0, null, hrasConn);
		if (err != WinError.ERROR_SUCCESS) {
			if (hrasConn.getValue() != null) Rasapi32.INSTANCE.RasHangUp(hrasConn.getValue());
			throw new Ras32Exception(err);
		}
		return hrasConn.getValue();
	}

	/**
	 * Dial a phone book entry by name (Asynchronously - callback type 2)
	 * @param entryName The phone book entry name
         * @param func2
	 * @return the HRASCONN for this connection
	 * @throws Ras32Exception errors
	 */
	public static HANDLE dialEntry(String entryName, RasDialFunc2 func2) throws Ras32Exception {
		// get the RAS Credentials
		RASCREDENTIALS.ByReference credentials = new RASCREDENTIALS.ByReference();
		synchronized (phoneBookMutex) {
			credentials.dwMask = WinRas.RASCM_UserName | WinRas.RASCM_Password | WinRas.RASCM_Domain;
			int err = Rasapi32.INSTANCE.RasGetCredentials(null, entryName, credentials);
			if (err != WinError.ERROR_SUCCESS) throw new Ras32Exception(err);
		}

		// set the dialing parameters
		RASDIALPARAMS.ByReference rasDialParams = new RASDIALPARAMS.ByReference();
		System.arraycopy(entryName.toCharArray(), 0, rasDialParams.szEntryName, 0, entryName.length());
		System.arraycopy(credentials.szUserName, 0, rasDialParams.szUserName, 0, credentials.szUserName.length);
		System.arraycopy(credentials.szPassword, 0, rasDialParams.szPassword, 0, credentials.szPassword.length);
		System.arraycopy(credentials.szDomain, 0, rasDialParams.szDomain, 0, credentials.szDomain.length);

		// dial
		HANDLEByReference hrasConn = new HANDLEByReference();
		int err = Rasapi32.INSTANCE.RasDial(null, null, rasDialParams, 2, func2, hrasConn);
		if (err != WinError.ERROR_SUCCESS) {
			if (hrasConn.getValue() != null) Rasapi32.INSTANCE.RasHangUp(hrasConn.getValue());
			throw new Ras32Exception(err);
		}
		return hrasConn.getValue();
	}

}
