/*++


  Author:

  Doron J. Holan (doronh), 1-22-1998
  --*/

#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif

#include <windows.h>

#include <msports.h>
#include <tchar.h>

#define GROWTH_VALUE         1024

#define BITS_INA_BYTE        8

typedef struct _DB_INFO {

    HANDLE  RegChangedEvent;
    HANDLE  AccessMutex;

    HKEY    DBKey;

    PBYTE   Ports;
    ULONG   PortsLength;
} DB_INFO, * PDB_INFO;

#define HandleToDBInfo(h) ((PDB_INFO) (h))
#define IsEventSignalled(hevent) (WaitForSingleObject(hevent, 0) == WAIT_OBJECT_0)
#define SanityCheckComNumber(num) { if (num > COMDB_MAX_PORTS_ARBITRATED) return ERROR_INVALID_PARAMETER; }
#define SanityCheckDBInfo(dbi) { if ((HANDLE) dbi == INVALID_HANDLE_VALUE) return ERROR_INVALID_PARAMETER; }


const TCHAR szMutexName[] = _T("ComPortNumberDatabaseMutexObject");
const TCHAR szComDBName[] = _T("ComDB");
const TCHAR szComDBMerge[] = _T("ComDB Merge");
const TCHAR szComDBPath[] = _T("System\\CurrentControlSet\\Control\\COM Name Arbiter");
const TCHAR szComDBPathOld[] = _T("System\\CurrentControlSet\\Services\\Serial");

#ifdef malloc
#undef malloc
#endif
#define malloc(size) LocalAlloc(LPTR, (size))

#ifdef free
#undef free
#endif 
#define free LocalFree

VOID
DestroyDBInfo(
     PDB_INFO DBInfo
     )
{
    if (DBInfo->AccessMutex && 
        DBInfo->AccessMutex != INVALID_HANDLE_VALUE) {
        CloseHandle(DBInfo->AccessMutex);
    }

    if (DBInfo->RegChangedEvent && 
        DBInfo->RegChangedEvent != INVALID_HANDLE_VALUE) {
        CloseHandle(DBInfo->RegChangedEvent);
    }

    if (DBInfo->DBKey && 
        DBInfo->DBKey != (HKEY) INVALID_HANDLE_VALUE) {
        RegCloseKey(DBInfo->DBKey);     
    }

    if (DBInfo->Ports) {
        free(DBInfo->Ports);
    }

    free(DBInfo);
}

LONG
CreationFailure (
     PHCOMDB  PHComDB,
     PDB_INFO DBInfo
     )
{
    if (DBInfo->AccessMutex != 0) 
        ReleaseMutex(DBInfo->AccessMutex);
    DestroyDBInfo(DBInfo);
    *PHComDB = (HCOMDB) INVALID_HANDLE_VALUE;
    return ERROR_ACCESS_DENIED;
}

VOID
RegisterForNotification(
    PDB_INFO DBInfo
    )
{
    ResetEvent(DBInfo->RegChangedEvent);
    if (RegNotifyChangeKeyValue(DBInfo->DBKey,
                                FALSE,
                                REG_NOTIFY_CHANGE_LAST_SET,
                                DBInfo->RegChangedEvent,
                                TRUE) != ERROR_SUCCESS) {
        //
        // Can't get a notification of when the DB is changed so close the handle
        // and we must update the DB at every access no matter what
        //
        CloseHandle(DBInfo->RegChangedEvent);
        DBInfo->RegChangedEvent = INVALID_HANDLE_VALUE;
    }
}

BOOL
ResizeDatabase(
    PDB_INFO DBInfo,
    ULONG    NumberPorts
    )
{
    PBYTE newPorts = NULL;
    ULONG newPortsLength;

    if (DBInfo->Ports) {
        newPortsLength = NumberPorts / BITS_INA_BYTE;
        newPorts = (PBYTE) malloc(newPortsLength * sizeof(BYTE));

        if (newPorts) {
            memcpy(newPorts, DBInfo->Ports, DBInfo->PortsLength);
            free(DBInfo->Ports);
            DBInfo->Ports = newPorts;
            DBInfo->PortsLength = newPortsLength;

            return TRUE;
        }
        else {
            return FALSE;
        }
    }
    else {
        //
        // Just alloc and be done with it
        //
        DBInfo->PortsLength = NumberPorts / BITS_INA_BYTE;
        DBInfo->Ports = (PBYTE) malloc(DBInfo->PortsLength * sizeof(BYTE));

        return DBInfo->Ports ? TRUE : FALSE;
    }
}

LONG
WINAPI
ComDBOpen (
    PHCOMDB PHComDB
    )
/*++

Routine Description:

    Opens name data base, and returns a handle to be used in future calls.

Arguments:

    None.

Return Value:

    INVALID_HANDLE_VALUE if the call fails, otherwise a valid handle

    If INVALID_HANDLE_VALUE, call GetLastError() to get details (??)

--*/
{
    PDB_INFO dbInfo = malloc(sizeof(DB_INFO));
    DWORD    type, size, disposition = 0x0;
    BOOLEAN  migrated = FALSE;
    LONG     res;
    BYTE     merge[COMDB_MIN_PORTS_ARBITRATED / BITS_INA_BYTE /* 32 */]; 

    if (dbInfo == 0) {
        *PHComDB = (HCOMDB) INVALID_HANDLE_VALUE;
        return ERROR_ACCESS_DENIED;
    }

    dbInfo->AccessMutex = CreateMutex(NULL, FALSE, szMutexName);

    if (dbInfo->AccessMutex == 0) {
        return CreationFailure(PHComDB, dbInfo);
    }

    //
    // Enter the mutex so we can guarantee only one thread pounding on the reg
    // key at once
    //
    WaitForSingleObject(dbInfo->AccessMutex, INFINITE);

    if (RegCreateKeyEx(HKEY_LOCAL_MACHINE,
                       szComDBPath, 
                       0,
                       (TCHAR *) NULL, 
                       REG_OPTION_NON_VOLATILE,
                       KEY_ALL_ACCESS | KEY_NOTIFY,
                       (LPSECURITY_ATTRIBUTES) NULL,
                       &dbInfo->DBKey,
                       &disposition) != ERROR_SUCCESS) {
        //
        // Try again w/out notification caps
        //
        if (RegCreateKeyEx(HKEY_LOCAL_MACHINE,
                           szComDBPath,
                           0,
                           (TCHAR *) NULL,
                           REG_OPTION_NON_VOLATILE,
                           KEY_ALL_ACCESS,
                           (LPSECURITY_ATTRIBUTES) NULL,
                           &dbInfo->DBKey, 
                           &disposition) != ERROR_SUCCESS) {
            return CreationFailure(PHComDB, dbInfo);
        }

        dbInfo->RegChangedEvent = INVALID_HANDLE_VALUE;
    }
    else {
        dbInfo->RegChangedEvent = CreateEvent(NULL, TRUE, FALSE, NULL);

        if (dbInfo->RegChangedEvent == 0) {
            dbInfo->RegChangedEvent = INVALID_HANDLE_VALUE;
        }
    }

    if (disposition == REG_CREATED_NEW_KEY) {
        //
        // Must migrate the previous values from the old com db path
        //
        HKEY hOldDB;

        if (RegOpenKeyEx(HKEY_LOCAL_MACHINE,
                         szComDBPathOld, 
                         0,
                         KEY_ALL_ACCESS,
                         &hOldDB) == ERROR_SUCCESS &&
            RegQueryValueEx(hOldDB,
                            szComDBName,
                            0,
                            &type,
                            NULL,
                            &dbInfo->PortsLength) == ERROR_SUCCESS) {

            //
            // The old value is still there, get its contents, copy it to the 
            // new location and delete the old value
            //
            migrated = TRUE;
            ResizeDatabase(dbInfo, dbInfo->PortsLength * BITS_INA_BYTE);
    
            size = dbInfo->PortsLength;

            res = RegQueryValueEx(hOldDB,
                                  szComDBName,
                                  0,
                                  &type,
                                  (PBYTE) dbInfo->Ports,
                                  &size);

            RegDeleteValue(hOldDB, szComDBName);

            //
            // The value does not exist, write it out
            //
            if (RegSetValueEx(dbInfo->DBKey,
                              szComDBName,
                              0,
                              REG_BINARY,
                              dbInfo->Ports,
                              dbInfo->PortsLength) != ERROR_SUCCESS) {

                RegCloseKey(hOldDB);
                return CreationFailure(PHComDB, dbInfo);
            }

            RegCloseKey(hOldDB);
        }

    }

    //
    // If we haven't migrated values from the old path, then either create a 
    // new chunk or read in values previously written
    //
    if (!migrated) {
        res = RegQueryValueEx(dbInfo->DBKey,
                              szComDBName,
                              0,
                              &type,
                              NULL,
                              &dbInfo->PortsLength);
    
        if (res == ERROR_FILE_NOT_FOUND) {
            ResizeDatabase(dbInfo, COMDB_MIN_PORTS_ARBITRATED); 
    
            //
            // The value does not exist, write it out
            //
            res = RegSetValueEx(dbInfo->DBKey,
                                szComDBName,
                                0,
                                REG_BINARY,
                                dbInfo->Ports,
                                dbInfo->PortsLength);
                                
            if (res != ERROR_SUCCESS) {
                return CreationFailure(PHComDB, dbInfo);
            }
        }
        else if (res == ERROR_MORE_DATA || res != ERROR_SUCCESS || type != REG_BINARY) {
            return CreationFailure(PHComDB, dbInfo);
        }
        else if (res == ERROR_SUCCESS) {
            ResizeDatabase(dbInfo, dbInfo->PortsLength * BITS_INA_BYTE);
    
            size = dbInfo->PortsLength;
            res = RegQueryValueEx(dbInfo->DBKey,
                                  szComDBName,
                                  0,
                                  &type,
                                  (PBYTE) dbInfo->Ports,
                                  &size);
        }
    }
    
    size = sizeof(merge);
    if (RegQueryValueEx(dbInfo->DBKey,
                        szComDBMerge,
                        0,
                        &type,
                        (PBYTE) merge,
                        &size) == ERROR_SUCCESS &&
        size <= dbInfo->PortsLength) {

        int i;

        for (i = 0 ; i < COMDB_MIN_PORTS_ARBITRATED / BITS_INA_BYTE; i++) {
            dbInfo->Ports[i] |= merge[i];
        }
        
        RegDeleteValue(dbInfo->DBKey, szComDBMerge);

        RegSetValueEx(dbInfo->DBKey,
                      szComDBName,
                      0,
                      REG_BINARY,
                      dbInfo->Ports,
                      dbInfo->PortsLength);
    }

    if (dbInfo->RegChangedEvent != INVALID_HANDLE_VALUE) {
        RegisterForNotification(dbInfo);
    }

    ReleaseMutex(dbInfo->AccessMutex);

    //
    // All done!  phew...
    //
    *PHComDB = (HCOMDB) dbInfo;
    return ERROR_SUCCESS;

}

LONG
WINAPI
ComDBClose (
    HCOMDB HComDB
    )
/*++

Routine Description:

    frees a handle to the database returned from OpenComPortDataBase

Arguments:

    Handle returned from OpenComPortDataBase.

Return Value:

    None

--*/
{
    PDB_INFO dbInfo = HandleToDBInfo(HComDB);

    SanityCheckDBInfo(dbInfo);
    DestroyDBInfo(dbInfo);

    return ERROR_SUCCESS;
}

BOOL
EnterDB(
    PDB_INFO DBInfo
    )
{
    BOOL eventSignalled = FALSE;
    LONG res;
    DWORD type, size;

    WaitForSingleObject(DBInfo->AccessMutex, INFINITE);
    
    if (DBInfo->RegChangedEvent == INVALID_HANDLE_VALUE ||
        (eventSignalled = IsEventSignalled(DBInfo->RegChangedEvent))) {

        size = 0;
        res = RegQueryValueEx(DBInfo->DBKey,
                              szComDBName,
                              0,
                              &type,
                              0,
                              &size);

        //
        // Couldn't update the DB ... fail
        // 
        if (res != ERROR_SUCCESS || type != REG_BINARY) {
            ReleaseMutex(DBInfo->AccessMutex);
            return FALSE;
        }

        if (size != DBInfo->PortsLength) {
            ResizeDatabase(DBInfo, size * BITS_INA_BYTE);
        }

        RegQueryValueEx(DBInfo->DBKey,
                        szComDBName,
                        0,
                        &type,
                        DBInfo->Ports,
                        &size);

        //
        // Reregister the notification with the registry
        // 
        if (eventSignalled) {
            RegisterForNotification(DBInfo);
        }
    }

    return TRUE;
}

LONG
LeaveDB(
    PDB_INFO DBInfo,
    BOOL     CommitChanges
    )
{
    LONG retVal = ERROR_SUCCESS;

    if (CommitChanges) {
        if (RegSetValueEx(DBInfo->DBKey,
                          szComDBName,
                          0,
                          REG_BINARY,
                          DBInfo->Ports,
                          DBInfo->PortsLength) != ERROR_SUCCESS) {
            retVal = ERROR_CANTWRITE;
        }

        //
        // The setting of the value in the reg signals the event...but we don't 
        // need to resync w/the reg off of this change b/c it is our own!  Instead
        // reset the event and rereg for the event
        //
        if (DBInfo->RegChangedEvent != INVALID_HANDLE_VALUE) {
            RegisterForNotification(DBInfo);
        }
    }

    ReleaseMutex(DBInfo->AccessMutex);
    return retVal;
}

VOID
GetByteAndMask(
    PDB_INFO DBInfo,
    DWORD    ComNumber,
    PBYTE    *Byte,
    PBYTE    Mask
    )
{
    ComNumber--;
    *Byte = DBInfo->Ports + (ComNumber / BITS_INA_BYTE);
    *Mask = 1 << (ComNumber % BITS_INA_BYTE);
}

LONG
WINAPI
ComDBGetCurrentPortUsage (
    HCOMDB   HComDB,
    PBYTE    Buffer,
    DWORD    BufferSize,
    ULONG    ReportType, 
    LPDWORD  MaxPortsReported
    )
/*++
    
    Handle requests that require no synch w/DB first.

  --*/
{
    PDB_INFO dbInfo = HandleToDBInfo(HComDB);
    PBYTE    curSrc, curDest, endDest;
    BYTE     mask;

    SanityCheckDBInfo(dbInfo);

    if (!EnterDB(dbInfo))  {
        return ERROR_NOT_CONNECTED;
    }

    if (Buffer == 0) {
        if (!MaxPortsReported) {
            LeaveDB(dbInfo, FALSE);
            return ERROR_INVALID_PARAMETER;
        }
        else {
            *MaxPortsReported = dbInfo->PortsLength * BITS_INA_BYTE;
            return LeaveDB(dbInfo, FALSE);
        }
    }

    if (ReportType == CDB_REPORT_BITS) {
        if (BufferSize > dbInfo->PortsLength) {
            BufferSize = dbInfo->PortsLength;
        }
        memcpy(Buffer, dbInfo->Ports, BufferSize);
        if (MaxPortsReported) {
            *MaxPortsReported = BufferSize * BITS_INA_BYTE;
        }
    }
    else if (ReportType == CDB_REPORT_BYTES) {
        if (BufferSize > dbInfo->PortsLength * BITS_INA_BYTE) {
            BufferSize = dbInfo->PortsLength * BITS_INA_BYTE;
        }

        curSrc = dbInfo->Ports;
        endDest = Buffer + BufferSize;
        curDest = Buffer;

        for (mask = 1; curDest != endDest; curDest++) {
            *curDest = (*curSrc & mask) ? 1 : 0;
            if (mask & 0x80) {
                mask = 0x1;
                curSrc++;
            }
            else
                mask <<= 1;
        }
    }
    else {
        LeaveDB(dbInfo, FALSE);
        return ERROR_INVALID_PARAMETER;
    }

    return LeaveDB(dbInfo, FALSE);
}

LONG
WINAPI
ComDBClaimNextFreePort (
    HCOMDB   HComDB,
    LPDWORD  ComNumber
    )
/*++

Routine Description:

    returns the first free COMx value

Arguments:

    Handle returned from OpenComPortDataBase.

Return Value:


    returns ERROR_SUCCESS if successful. or other ERROR_ if not

    if successful, then ComNumber will be that next free com value and claims it in the database


--*/
{
    PDB_INFO dbInfo = HandleToDBInfo(HComDB);
    DWORD    num;
    BOOL     commit = FALSE;
    PBYTE    curSrc, srcEnd;
    BYTE     mask;
    LONG     ret;

    SanityCheckDBInfo(dbInfo);

    if (!EnterDB(dbInfo)) {
        return ERROR_NOT_CONNECTED;
    }

    curSrc = dbInfo->Ports;
    srcEnd = curSrc + dbInfo->PortsLength;

    for (num = 3, mask = 0x4; curSrc != srcEnd; num++) {
        if (!(*curSrc & mask)) {
            *ComNumber = num;
            *curSrc |= mask;
            commit = TRUE;
            break;
        }
        else if (mask & 0x80) {
            mask = 0x1;
            curSrc++;
        }
        else {
            mask <<= 1;
        }
    }

    if (curSrc == srcEnd && !commit && num < COMDB_MAX_PORTS_ARBITRATED) {
        // DB entirely full
        ResizeDatabase(dbInfo, ((num / GROWTH_VALUE) + 1) * GROWTH_VALUE);
        *ComNumber = num;

        GetByteAndMask(dbInfo, num, &curSrc, &mask);
        *curSrc |= mask;    
        commit = TRUE;
    }

    ret = LeaveDB(dbInfo, commit);
    if (!commit) {
        ret = ERROR_NO_LOG_SPACE;
    }

    return ret;
}

LONG
WINAPI
ComDBClaimPort (
    HCOMDB   HComDB,
    DWORD    ComNumber,
    BOOL     ForceClaim,
    PBOOL    Forced
    )
/*++

Routine Description:

    Attempts to claim a com name in the database

Arguments:

    DataBaseHandle - returned from OpenComPortDataBase.

    ComNumber      - The port value to be claimed

    Force          - If TRUE, will force the port to be claimed even if in use already



Return Value:


    returns ERROR_SUCCESS if port name was not already claimed, or if it was claimed
                          and Force was TRUE.

    ERROR_SHARING_VIOLATION  if port name is use and Force is false


--*/
{
    PDB_INFO dbInfo = HandleToDBInfo(HComDB);
    PBYTE    curByte;
    BYTE     mask;
    BOOL     commit = TRUE;
    LONG     res;
    ULONG    newSize;
    
    BOOL f;
    if (!(Forced)) {
        Forced = &f;
    }
    SanityCheckComNumber(ComNumber);
    SanityCheckDBInfo(dbInfo);

    if (!EnterDB(dbInfo)) {
        return ERROR_NOT_CONNECTED;
    }

    if (ComNumber > dbInfo->PortsLength * BITS_INA_BYTE) {
        ResizeDatabase(dbInfo, ((ComNumber / GROWTH_VALUE) + 1) * GROWTH_VALUE);
    }

    GetByteAndMask(dbInfo, ComNumber, &curByte, &mask);

    if (*curByte & mask) {
        commit = FALSE;
        if (ForceClaim) {
            if (Forced)
                *Forced = TRUE;
        }
        else {
            res = LeaveDB(dbInfo, commit);
            if (res == ERROR_SUCCESS) {
                return ERROR_SHARING_VIOLATION;
            }
            else {
                return res;
            }
        }   
    }
    else {
        if (Forced)
            *Forced = FALSE;
        *curByte |= mask;
    }

    return LeaveDB(dbInfo, commit);
}

LONG
WINAPI
ComDBReleasePort (
    HCOMDB   HComDB, 
    DWORD    ComNumber
    )
/*++

Routine Description:

    un-claims the port in the database

Arguments:

    DatabaseHandle - returned from OpenComPortDataBase.

    ComNumber      - port to be unclaimed in database

Return Value:


    returns ERROR_SUCCESS if successful. or other ERROR_ if not


--*/
{
    PDB_INFO dbInfo = HandleToDBInfo(HComDB);
    PBYTE    byte;
    BYTE     mask;

    SanityCheckDBInfo(dbInfo);

    if (!EnterDB(dbInfo)) {
        return ERROR_NOT_CONNECTED;
    }

    if (ComNumber > dbInfo->PortsLength * BITS_INA_BYTE) {
        LeaveDB(dbInfo, FALSE);
        return ERROR_INVALID_PARAMETER;
    }

    GetByteAndMask(dbInfo, ComNumber, &byte, &mask);
    *byte &= ~mask;

    return LeaveDB(dbInfo, TRUE);
}

LONG
WINAPI
ComDBResizeDatabase (
    HCOMDB   HComDB, 
    DWORD    NewSize
    )
/*++

Routine Description:

    Resizes the database to the new size.  To get the current size, call
    ComDBGetCurrentPortUsage with a Buffer == NULL.

Arguments:

    DatabaseHandle - returned from OpenComPortDataBase.

    NewSize        - must be a multiple of 1024, with a max of 4096
    
Return Value:

    returns ERROR_SUCCESS if successful
            ERROR_BAD_LENGTH if NewSize is not greater than the current size or
                             NewSize is greater than COMDB_MAX_PORTS_ARBITRATED

--*/
{
    PDB_INFO dbInfo = HandleToDBInfo(HComDB);
    BOOL     commit = FALSE;

    SanityCheckDBInfo(dbInfo);

    if (NewSize % GROWTH_VALUE) {
        return ERROR_INVALID_PARAMETER;
    }

    if (!EnterDB(dbInfo)) {
        return ERROR_NOT_CONNECTED;
    }

    if (NewSize > COMDB_MAX_PORTS_ARBITRATED ||
        dbInfo->PortsLength * BITS_INA_BYTE >= NewSize) {
        LeaveDB(dbInfo, FALSE);
        return ERROR_BAD_LENGTH;
    }

    ResizeDatabase(dbInfo, NewSize);

    return LeaveDB(dbInfo, TRUE);
}