#include <wbemidl.h>
#include <wbemcomn.h>
#include <wbemutil.h>
#include <cominit.h>
#include "Pager.h"

#define LOGFILE_PROPNAME_NUMBER           L"PhoneNumber"
#define LOGFILE_PROPNAME_MESSAGE          L"Message"

#define LOGFILE_PROPNAME_ID               L"ID"
#define LOGFILE_PROPNAME_PORT             L"Port"
#define LOGFILE_PROPNAME_BAUDRATE         L"BaudRate"
#define LOGFILE_PROPNAME_SETUPSTR         L"ModemSetupString"
#define LOGFILE_PROPNAME_TIMEOUT          L"AnswerTimeout"

#define ACK '\x06'
#define NAK	'\x15'
#define EOT	'\x04'
#define NUL	'\x00'


HRESULT STDMETHODCALLTYPE CPagerConsumer::XProvider::FindConsumer(
                    IWbemClassObject* pLogicalConsumer,
                    IWbemUnboundObjectSink** ppConsumer)
{
    HRESULT hr = WBEM_E_FAILED;

    DEBUGTRACE((LOG_ESS, "Pager: Find Consumer\n"));

    CPagerSink* pSink = new CPagerSink(m_pObject->m_pControl);
    if (pSink)    
	{
		if (SUCCEEDED(hr = pSink->SetConsumer(pLogicalConsumer)))
			hr = pSink->QueryInterface(IID_IWbemUnboundObjectSink, (void**)ppConsumer);
		else
			pSink->Release();
	}
	else
        hr = WBEM_E_OUT_OF_MEMORY;
     

    DEBUGTRACE((LOG_ESS, "Pager: Find Consumer returning 0x%08X\n", hr));
    return hr;
}


HRESULT STDMETHODCALLTYPE CPagerConsumer::XInit::Initialize(
            LPWSTR, LONG, LPWSTR, LPWSTR, IWbemServices*, IWbemContext*, 
            IWbemProviderInitSink* pSink)
{
    pSink->SetStatus(0, 0);
    return 0;
}
    

void* CPagerConsumer::GetInterface(REFIID riid)
{
    if(riid == IID_IWbemEventConsumerProvider)
        return &m_XProvider;
    else if(riid == IID_IWbemProviderInit)
        return &m_XInit;
    else return NULL;
}



CPagerSink::~CPagerSink()
{
}

HRESULT CPagerSink::SetConsumer(IWbemClassObject* pLogicalConsumer)
{
    // Get the information
    // ===================

    HRESULT hres;
    VARIANT v;
    VariantInit(&v);

    hres = pLogicalConsumer->Get(LOGFILE_PROPNAME_NUMBER, 0, &v, NULL, NULL);
    if(FAILED(hres) || V_VT(&v) != VT_BSTR)
        return WBEM_E_INVALID_PARAMETER;
    m_phoneNumber = V_BSTR(&v);
    VariantClear(&v);

    hres = pLogicalConsumer->Get(LOGFILE_PROPNAME_MESSAGE, 0, &v, NULL, NULL);
    if(FAILED(hres) || V_VT(&v) != VT_BSTR)
        return WBEM_E_INVALID_PARAMETER;
    m_messageTemplate.SetTemplate(V_BSTR(&v));
    VariantClear(&v);

	hres = pLogicalConsumer->Get(LOGFILE_PROPNAME_ID, 0, &v, NULL, NULL);
    if(FAILED(hres) || V_VT(&v) != VT_BSTR)
        return WBEM_E_INVALID_PARAMETER;
    m_pagerID = V_BSTR(&v);
    VariantClear(&v);

	hres = pLogicalConsumer->Get(LOGFILE_PROPNAME_PORT, 0, &v, NULL, NULL);
    if(FAILED(hres) || V_VT(&v) != VT_BSTR)
        return WBEM_E_INVALID_PARAMETER;
    m_port = V_BSTR(&v);
    VariantClear(&v);

	hres = pLogicalConsumer->Get(LOGFILE_PROPNAME_BAUDRATE, 0, &v, NULL, NULL);
	if (SUCCEEDED(hres))
	{
		if (V_VT(&v) == VT_I4)
			m_baudRate = V_I4(&v);
		else if (V_VT(&v) == VT_NULL)
			m_baudRate = INVALID_BAUD_RATE;
		else
			return WBEM_E_INVALID_PARAMETER;    
	}
	else
		return hres;
    VariantClear(&v);

	hres = pLogicalConsumer->Get(LOGFILE_PROPNAME_TIMEOUT, 0, &v, NULL, NULL);
	if (SUCCEEDED(hres))
	{
		if (V_VT(&v) == VT_I4)
			m_timeout = 1000 * V_I4(&v);
		else if (V_VT(&v) == VT_NULL)
			m_timeout = 30000;
		else
			return WBEM_E_INVALID_PARAMETER;    
	}
	else
		return hres;
    VariantClear(&v);

	hres = pLogicalConsumer->Get(LOGFILE_PROPNAME_SETUPSTR, 0, &v, NULL, NULL);
    if(FAILED(hres) || !((V_VT(&v) == VT_BSTR) || (V_VT(&v) == VT_NULL)))
        return WBEM_E_INVALID_PARAMETER;
    m_modemSetupString = V_BSTR(&v);
	m_modemSetupString += L"\r";
    VariantClear(&v);

    return hres;
}

HRESULT STDMETHODCALLTYPE CPagerSink::XSink::IndicateToConsumer(
            IWbemClassObject* pLogicalConsumer, long lNumObjects, 
            IWbemClassObject** apObjects)
{    
    HRESULT hr = WBEM_S_NO_ERROR;
    
    DEBUGTRACE((LOG_ESS, "Pager: CPagerSink::IndicateToConsumer %d objects\n", lNumObjects));
        
    for (int i = 0; i < lNumObjects; i++)
    {
        // Apply the template to the event
        // ===============================
        BSTR strText = m_pObject->m_messageTemplate.Apply(apObjects[i]);

        // call somebody & let 'em know about it....
        if(strText)
        {
            hr = m_pObject->RingMeUp(strText);
            SysFreeString(strText);
        }
        else
            hr = WBEM_E_FAILED;
    }

    return hr;
}        

// listens to the modem for a while,
// searches for the string 'OK' in the string read
// modem should echo back OK if everything's hunky dory
HRESULT CPagerSink::IsOK(HANDLE hPort)
{
	HRESULT hr = WBEM_E_FAILED;
	SetTimeout(hPort, 1000);

    // monitor the return from the modem
	BYTE buffer[101];
	ZeroMemory(buffer, 101);
	DWORD dwErr = 0;
	DWORD bytesWryt;
	int nTries = 0;

	while (FAILED(hr) &&
		   ReadFile(hPort, &buffer, 100, &bytesWryt, NULL) &&
		   (nTries++ < 3))
				if (strstr(_strupr((char*)buffer), "OK"))
					hr = WBEM_S_NO_ERROR;

	if (SUCCEEDED(hr))
		DEBUGTRACE((LOG_ESS, "Pager: modem responds OK\n"));
	else
		ERRORTRACE((LOG_ESS, "Pager: modem does not respond OK (%s)\n", buffer));

	return hr;
}

// listens to the modem for a while,
// searches for the ACK char
bool CPagerSink::GotACK(HANDLE hPort)
{
	return (GetResponse(hPort) == ACK);	
}

// listens to the modem for a while,
// searches for the NAK char
bool CPagerSink::GotNAK(HANDLE hPort)
{
	return (GetResponse(hPort) == NAK);	
}

// listens to the modem for a while,
// searches for the string 'CONNECT' in the string read
// caller should set timeout to a fairly large number before calling this
HRESULT CPagerSink::IsConnected(HANDLE hPort)
{
	HRESULT hr = WBEM_E_FAILED;

    // monitor the return from the modem
	BYTE buffer[101];
	ZeroMemory(buffer, 101);
	DWORD dwErr = 0;
	DWORD bytesWryt;
	if (ReadFile(hPort, &buffer, 100, &bytesWryt, NULL))
	{
		if (strstr(_strupr((char*)buffer), "CONNECT"))
		{
			DEBUGTRACE((LOG_ESS, "Pager: connected (%s)\n", buffer));
			hr = WBEM_S_NO_ERROR;
		}
		else
			ERRORTRACE((LOG_ESS, "Pager: Failed to connect\"%s\"", buffer));
	}

	return hr;
}

// listens to the modem for a while,
// searches for the string that signifies we're ready to go
bool CPagerSink::ReadyToProceed(HANDLE hPort)
{
	bool bRet = false;
	SetTimeout(hPort, 10000);

    // monitor the return from the modem
	BYTE buffer[101];
	ZeroMemory(buffer, 101);
	DWORD dwErr = 0;
	DWORD bytesWryt;
	int nTries = 0;

	while (!bRet && (nTries++ < 3) && ReadFile(hPort, &buffer, 100, &bytesWryt, NULL))
		if (strstr((char*)buffer, "[p"))
			bRet = true;
		else
			ZeroMemory(buffer, 101);

	if (bRet)
		DEBUGTRACE((LOG_ESS, "Pager: Message go-ahead\n"));
	else
		ERRORTRACE((LOG_ESS, "Pager: FAILED to receive go-ahead\"%s\"\n", buffer));		

	return bRet;
}


// if timeout == 0 (the default) it will 
// set a (hopefully) reasonable timeout
// for waiting for a response from the modem itself
// timeout is in milliseconds
void CPagerSink::SetTimeout(HANDLE hPort, DWORD timeout)
{
	COMMTIMEOUTS commTimeout;
	
	if (timeout == 0)
	{
		commTimeout.WriteTotalTimeoutConstant   = 100; 
		commTimeout.ReadTotalTimeoutConstant    = 100; 		
	}
	else
	{
		commTimeout.WriteTotalTimeoutConstant   = timeout; 
		commTimeout.ReadTotalTimeoutConstant    = timeout; 		
	}

	commTimeout.ReadIntervalTimeout         = 20; 
	commTimeout.ReadTotalTimeoutMultiplier  = 10; 
	commTimeout.WriteTotalTimeoutMultiplier = 10; 

	SetCommTimeouts(hPort, &commTimeout);
}

// configure modem using both our 'well known settings'
// and the modem control string in the MOF
// upon successful return *ppDCB points to a DCB representing
// the orginal state of the modem before we whacked it.
// caller's responsibility to delete the ppDCB
HRESULT CPagerSink::ConfigureModem(HANDLE hPort, DCB** ppDCB)
{
	DEBUGTRACE((LOG_ESS, "Pager: CPagerSink::ConfigureModem\n"));
	
	HRESULT hr = WBEM_E_FAILED;

	*ppDCB = new DCB;
	if (!*ppDCB)
		hr = WBEM_E_OUT_OF_MEMORY;
	else
	{
		if (!GetCommState(hPort, *ppDCB))
			hr = WBEM_E_FAILED;
		else
		{
			SetTimeout(hPort);

			// we're here - let's set some settings
			// first copy the existing settings...
			DCB dcb = **ppDCB;

			// ...and then tweak the bits we want tweaked
			if (m_baudRate != INVALID_BAUD_RATE)
				dcb.BaudRate = m_baudRate;
			
			// following setting is per the TAP standard
			// E-7-1
			dcb.Parity = 2;   // even parity
			dcb.ByteSize = 7; // seven bits
			dcb.StopBits = 0; // per the spec, 0 == "one stop bit"
			dcb.fParity = 1;  // enable parity checking

			if (SetCommState(hPort, &dcb))
			{
				hr = WBEM_S_NO_ERROR;
				DWORD bytesWryt;
				
				// Is there an echo in here?
				// if so - turn it OFF!
				const char* echoOff = "ATE0\r";
				WriteFile(hPort, echoOff, strlen(echoOff), &bytesWryt, NULL);

				// this will clear the buffer, but we won't write it down...
				IsOK(hPort);						

				if (SUCCEEDED(hr) && (m_modemSetupString.Length() != 0))
				{
					LPSTR pSetup = m_modemSetupString.GetLPSTR();
					if (!pSetup)
						hr = WBEM_E_OUT_OF_MEMORY;
					else
					{					
						// setup string is problem of originator, we'll believe it:
						WriteFile(hPort, pSetup, strlen(pSetup), &bytesWryt, NULL);
						delete pSetup;

						hr = IsOK(hPort);
					}
				}
			}
			else
				ERRORTRACE((LOG_ESS, "Pager: SetCommState Failed: %d", GetLastError()));
		}
	}

	return hr;
}

HRESULT CPagerSink::DialUp(HANDLE hPort)
{
	DEBUGTRACE((LOG_ESS,"Pager: CPagerSink::DialUp\n"));
	
	HRESULT hr = WBEM_E_FAILED;
																	   
	LPSTR pNumber = m_phoneNumber.GetLPSTR();

	if (!pNumber)
		hr = WBEM_E_OUT_OF_MEMORY;
	else
	{
		char buf[100];
		DWORD bytesWryt;
		wsprintf(buf, "ATDT%s\r", pNumber);
		WriteFile(hPort, buf, strlen(buf), &bytesWryt, NULL); 

		SetTimeout(hPort, m_timeout);
		hr = IsConnected(hPort);

		delete pNumber;

		SetTimeout(hPort);
	}

	return hr;
}

// SHOULD return ACK, NAK, EOT, or NUL
// will keep trying until it gets one or another;
char CPagerSink::GetResponse(HANDLE hPort)
{
	SetTimeout(hPort, 5000);

    // monitor the return from the modem
	BYTE buffer[101];
	ZeroMemory(buffer, 101);
	DWORD dwErr = 0;
	DWORD bytesWryt;
	int nTries = 0;
	
	// see if we see a response we might be interested in
	while ((nTries++ < 3) && ReadFile(hPort, &buffer, 100, &bytesWryt, NULL))
	{
		if (strchr((const char *)buffer, ACK))
		{
			DEBUGTRACE((LOG_ESS, "Pager: service responds ACK\n"));
			return ACK;			
		}
		else if (strchr((const char *)buffer, NAK))
		{
			DEBUGTRACE((LOG_ESS, "Pager: service responds NAK\n"));
			return NAK;
		}
		else if (strchr((const char *)buffer, EOT))
		{
			DEBUGTRACE((LOG_ESS, "Pager: service responds EOT\n"));
			return EOT;
		}
		else
			// go back, jack, and do it again...
			ZeroMemory(buffer, 101); 
	}
	
	// welp, tried three times & couldn't get it.  bail.
	return NUL;
};

// gonna sit here and listen for a prompt
// but we're gonna proceed even if we don't...
void CPagerSink::GetPrompt(HANDLE hPort)
{
	int nAttempts = 0;
	bool gotPrompt = false;
	DWORD bytesWrytten;
	// per TAP spec - they're supposed to respond within ONE second
	SetTimeout(hPort, 1500);
	char buf[101];

	do 
	{
		WriteFile(hPort, "\r", 1, &bytesWrytten, NULL);
		ZeroMemory(buf, 101);
		if (ReadFile(hPort, &buf, 100, &bytesWrytten, NULL))
			if (strstr(_strupr((char*)buf), "ID="))
				gotPrompt = true;
	}
	while ((++nAttempts < 3) && !gotPrompt);

	if (gotPrompt)
		DEBUGTRACE((LOG_ESS, "Pager: Received prompt\n"));
	else
		ERRORTRACE((LOG_ESS, "Pager: Did not receive prompt\n"));
}

HRESULT CPagerSink::Login(HANDLE hPort)
{
	HRESULT hr = WBEM_E_FAILED;
	
	GetPrompt(hPort);

	// this *should* be universal.  Maybe not, too...
	char* loginString = "\x1BPG1\r";
	DWORD bytesWrytten;
	
	SetTimeout(hPort, 10000);
	WriteFile(hPort, loginString, strlen(loginString), &bytesWrytten, NULL);
	
	if (GotACK(hPort))
	{
		hr = WBEM_S_NO_ERROR;
		DEBUGTRACE((LOG_ESS, "Pager: Received ACK on login\n"));
	}
	else
		ERRORTRACE((LOG_ESS, "Pager: Failed to receive ACK on Login\n"));
	
	return hr;
}

HRESULT CPagerSink::SendMessage(HANDLE hPort, BSTR message)
{
	HRESULT hr = WBEM_E_FAILED;

	if (ReadyToProceed(hPort))
	{
		DEBUGTRACE((LOG_ESS, "Pager: CPagerSink::SendMessage\n"));

		// alloc enough buffer for message that's all double-byte chars.
		// not that the pager is likely to UNDERSTAND, but let's not embarrass ourselves with an overwrite.
		// also room for null teminator, the separators that TAP will want, and some paranoia-padding
		// format: <STX>[pager id]<CR>[message]<ETX>[3 char checksum]<CR>

		char* pMessage = new char[2*wcslen(message) + 2*wcslen(m_pagerID) +12];
		if (!pMessage)
			hr = WBEM_E_OUT_OF_MEMORY;
		else
		{
			// djinn up the message per format
			sprintf(pMessage, "\x02%S\x0D%S\x03", m_pagerID, message);
			int nlen = strlen(pMessage);
			int sum = 0;

			// compute checksum:
			for (int i = 0; i < nlen; i++)
				sum += pMessage[i];
			
			// only want the lower three bytes:
			sum &= 0x0FFF;

			// append to message:
			// it's hex: but 'A' through 'E' are ':' through '?' 
			// (look at ASCII table - it makes sense)
			// there's probably a cute way to do this in a loop,
			// but what the heck, I'm lazy (or stoopid):
			pMessage[nlen +0] = (char) (0x30 + ((sum & 0x0F00) / 0x100));
			pMessage[nlen +1] = (char) (0x30 + ((sum & 0x00F0) / 0x10));
			pMessage[nlen +2] = (char) (0x30 + ((sum & 0x000F) / 0x1));
			pMessage[nlen +3] = '\x0D';
			pMessage[nlen +4] =	'\0';
				
			// we'll retry if we get a NAK
			DWORD bytesWrytten;
			int nTries = 0;
			do 
			{			
				DEBUGTRACE((LOG_ESS, "Pager: attempting send #%d\n", nTries +1));
				if (WriteFile(hPort, pMessage, strlen(pMessage), &bytesWrytten, NULL))
					hr = WBEM_S_NO_ERROR;
			}
			while (GotNAK(hPort) && (++nTries < 3));
			
			delete[] pMessage;
		} // if allocated memory
	}// if ready to proceed

	return hr;
}

// issue logout commands and hang up.
// won't bother listening for any response: 
// we don't care - we were just leaving, anyway.
void CPagerSink::LogOut(HANDLE hPort)
{
	DEBUGTRACE((LOG_ESS, "Pager: CPagerSink::LogOut\n"));
	DWORD bytesWrytten;
	
	char* logout = "\x04\r";
	WriteFile(hPort, logout, strlen(logout), &bytesWrytten, NULL);

	char* hangup = "ATH0";
	WriteFile(hPort, hangup, strlen(hangup), &bytesWrytten, NULL);
}


HRESULT CPagerSink::RingMeUp(BSTR message)
{
    DEBUGTRACE((LOG_ESS, "Pager: Paging with message \"%S\"\n", message));
    HRESULT hr = WBEM_E_FAILED;

	LPSTR port = m_port.GetLPSTR();
	if (!port)
		hr = WBEM_E_OUT_OF_MEMORY;
	else
	{
		HANDLE hPort;
		hPort = CreateFile(port, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
		delete port;
		port = NULL;
		 
		if (hPort == INVALID_HANDLE_VALUE)
		{
			ERRORTRACE((LOG_ESS, "Pager: CreateFile failed with %d\n", GetLastError()));
			// todo: investigate various failure modes
			// we ought to be able to distinguish between a modem that's in use
			// and one that doesn't exist, at the very least.
			hr = WBEM_E_FAILED;
			// hr = WBEM_E_INVALID_PARAMETER;
		}
		else
		{
			DCB* pDCBOld = NULL;
			if (SUCCEEDED(hr = ConfigureModem(hPort, &pDCBOld))	&&
				SUCCEEDED(hr = DialUp(hPort)) &&
				SUCCEEDED(hr = Login(hPort)))
			{
				hr = SendMessage(hPort, message);	
			}
			
			// even if we blew it somewhere along the line
			// we still want to hang up.
			// at worst we'll be sending commands to a modem that's already disconnected
			LogOut(hPort);

			if 	(pDCBOld)
			{			
				SetCommState(hPort, pDCBOld);
				delete pDCBOld;
				pDCBOld = NULL;
			}

			CloseHandle(hPort);
		}
	}

	return hr;
}

void* CPagerSink::GetInterface(REFIID riid)
{
    if (riid == IID_IWbemUnboundObjectSink)
        return &m_XSink;
    else 
        return NULL;
}