#define INCL_ERRORS
#define INCL_DOSMISC
#include <os2.h>

#define INCL_WTKMMF
#define INCL_WTKUTLFILE
#include <wtk.h>

#include <errno.h>
#include <malloc.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>

#include <sys/smutex.h>
#include <InnoTekLIBC/thread.h>

// use max 1000 buffers per thread (clamav)
#define MMAP_MAXFILES 1000

// define to pre-commit mmap'ed buffer and avoid except handler while debugging
#ifdef DEBUG
//#define DEBUG_2
#endif

#ifdef DEBUG
#include <stdarg.h>
inline static void debug_printf(const char *fmt, ...)
{
    va_list ap;
    va_start(ap, fmt);
    vfprintf(stderr, fmt, ap);
    va_end(ap);
}
#define DPRINTF(p) debug_printf p
#else
#define DPRINTF(p) do {} while(0)
#endif

static void mmapThreadDestructor(__LIBC_PTHREADTERMCBREGREC pRegRec, unsigned fFlags);
void mmap_thread_cleanup(void);
static void mmap_cleanup(void);

/** Thread Termination Registration Record. */
static __LIBC_THREADTERMCBREGREC    gThrdTermRegRec =
{
    NULL, 0, mmapThreadDestructor
};

/* ------------------------------------------------------------ */

/**
 * Check MMF resources at application exit
 */
static void mmap_cleanup(void)
{
    mmapThreadDestructor( NULL, 0);
}

/**
 * Callback function for thread termination.
 */
static void mmapThreadDestructor(__LIBC_PTHREADTERMCBREGREC pRegRec, unsigned fFlags)
{
    ULONG   ulrc;
    MMFINFO mi;

    DPRINTF(( "tid#%d mmapThreadDestructor: check MMF\n", _gettid()));

    // if MMF handler has been initialized for
    // this thread, terminate it
    if ((ulrc = WtkGetMmfInfo( &mi)) == NO_ERROR) {
        DPRINTF(( "tid#%d mmapThreadDestructor: MMF found, cleanup not done\n", _gettid()));
        ulrc = WtkTerminateMmf( mi.hmmf);
        DPRINTF(( "tid#%d mmapThreadDestructor: WtkTerminateMmf hmmf 0x%08x, rc=%u\n", _gettid(), mi.hmmf, ulrc));
    } else {
        DPRINTF(( "tid#%d mmapThreadDestructor: WtkGetMmfInfo failed rc=%u, no mapping or cleanup correctly done.\n", _gettid(), ulrc));
    }
}

/* ------------------------------------------------------------ */

// cleanup MMF resources at thread exit
void mmap_thread_cleanup(void)
{
    ULONG   ulrc;
    MMFINFO mi;

    DPRINTF(( "tid#%d mmap_thread_cleanup\n", _gettid()));

    // if MMF handler has been initialized for
    // this thread, terminate it
    if ((ulrc = WtkGetMmfInfo( &mi)) == NO_ERROR) {
        ulrc = WtkTerminateMmf( mi.hmmf);
        DPRINTF(( "tid#%d mmap_thread_cleanup: WtkTerminateMmf hmmf 0x%08x, rc=%u\n", _gettid(), mi.hmmf, ulrc));
    } else {
        DPRINTF(( "tid#%d mmap_thread_cleanup: WtWtkGetMmfInfo failed rc=%u, maybe no mmapping in this thread.\n", _gettid(), ulrc));
    }

}

/* ------------------------------------------------------------ */

void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset)
{
    void*   pvdata;
    APIRET  ulrc;
    ULONG   ulOpenFlags = 0;
    MMFINFO mi;
    HMMF    hmmf;
    static _smutex      smtxInit;
    static volatile int fInited;

    // check parameters
    DPRINTF(( "tid#%d mmap: fd=%d, prot=0x%08x, flags=0x%08x\n", _gettid(), fd, prot, flags));

    // - MAP_FIXED not supported, as MMF cannot
    //   determine the address of the allocated memory
    if ((flags & MAP_FIXED) == MAP_FIXED) {
        errno = EINVAL;
        return MAP_FAILED;
    }

    // - PROT_NONE not supported with MMF
    if (prot == PROT_NONE) {
        errno = ENOTSUP;
        return MAP_FAILED;
    }

    /*
     * Init the destructor if needed.
     */
    if (!fInited)
    {
        _smutex_request(&smtxInit);
        if (!fInited)
        {
            LONG  lReqCount = 0;
            ULONG ulCurMaxFH = 0;
#ifdef DEBUG
            // register exit handler for thread#1
            atexit( mmap_cleanup); 
            // register thread callback
            __libc_ThreadRegisterTermCallback(&gThrdTermRegRec);
#endif
            // adjust max file handles, klibc does for apps, but maybe we can reach
            // the limit here first since we use Dos* API
            ulrc = DosSetRelMaxFH( &lReqCount, &ulCurMaxFH);
            if (ulCurMaxFH < MMAP_MAXFILES)
                ulrc = DosSetMaxFH(MMAP_MAXFILES);
            // init done
            fInited = 1;
        }
        _smutex_release(&smtxInit);
    }

    // get handle to mmf
    ulrc = WtkGetMmfInfo( &mi);
    if (ulrc != NO_ERROR) {
        // initialize
        ulrc = WtkInitializeMmf( &hmmf, MMAP_MAXFILES);
        DPRINTF(( "tid#%d mmap: WtkInitializeMmf hmmf 0x%08x, rc=%u\n", _gettid(), hmmf, ulrc));
        if (ulrc != NO_ERROR) {
            DPRINTF(( "tid#%d mmap: WtkInitializeMmf failed, rc=%u\n", _gettid(), ulrc));
            return MAP_FAILED;
        }
    } else {
        hmmf = mi.hmmf;
    }

    // determine memory mode flag
    switch (prot & (PROT_READ|PROT_WRITE)) {
        case PROT_READ:
            ulOpenFlags = MMF_MEMORY_READONLY;
            break;

        case PROT_WRITE:
        case (PROT_READ|PROT_WRITE):
            ulOpenFlags = MMF_MEMORY_READWRITE;
            break;
    } // switch (prot & (PROT_READ|PROT_WRITE))

    // handle PROT_EXEC separately
    if ((prot & PROT_EXEC) == PROT_EXEC) {
        // if execute is required, use extra MMF flag
        // (under OS/2 Warp 4 and eComStation execute
        //  access requires readonly access only, we
        //  allow write access as well in that case)
        ulOpenFlags |= MMF_MEMORY_EXECUTE;
    }

    // determine access to the underlying file
    switch (flags & (MAP_PRIVATE|MAP_SHARED)) {
        case MAP_PRIVATE:
            ulOpenFlags |= MMF_ACCESS_READONLY;
            break;

        case MAP_SHARED:
            ulOpenFlags |= MMF_ACCESS_READWRITE;
            break;
    } // switch (flags & (MAP_PRIVATE|MAP_SHARED) {

    // be sure to use invalid fd for anonymous maps
    if ((flags & MAP_ANON) == MAP_ANON)
        fd = -1;

    // release memory on msync
    ulOpenFlags |= MMF_UPDATE_RELEASEMEM;

    // allocate file buffer in high memory
    ulOpenFlags |= MMF_ALLOC_HIGHMEM;

    // allocate buffer
    ulrc = WtkAllocMmfFileEx( hmmf, &pvdata, fd, ulOpenFlags, length, offset);
    DPRINTF(( "tid#%d mmap:  hmmf 0x%08x, fd=%d, pvdata=0x%08x, length=%u, offset=%llu, rc=%u\n", _gettid(), hmmf, fd, pvdata, length, offset, ulrc));

    switch (ulrc) {
        case ERROR_INVALID_PARAMETER:
             errno = EINVAL;
             break;
         case ERROR_ACCESS_DENIED:
             // access flags do not match the file handle
             errno = EACCES;
             break;
         case ERROR_INVALID_HANDLE:
             // MMF or file handle invalid
             errno = EBADF;
             break;
         case ERROR_NOT_ENOUGH_MEMORY:
             errno = ENOMEM;
             break;
         case ERROR_TOO_MANY_OPEN_FILES:
             errno = EMFILE;
             break;
    } // switch (ulrc)

    if (ulrc != NO_ERROR) {
        return MAP_FAILED;
    }

#ifdef DEBUG_2
    if (fd != -1)
        WtkCommitMmf( hmmf, pvdata, length);
#endif

    return pvdata;
}

/* ------------------------------------------------------------ */

int munmap(void *start, size_t length)
{
    APIRET  ulrc;
    MMFINFO mi;
    ULONG   ulPageSize = 4096;
    ULONG   ulPageFlags = 0;

    // get handle to mmf
    ulrc = WtkGetMmfInfo( &mi);
    if (ulrc != NO_ERROR) {
        DPRINTF(( "tid#%d munmap: PANIC, MMF not initialized! pvdata=0x%08x, len=%d\n", _gettid(), start, length));
        exit(1);
    }

    // query commit and permission flags of start page
    ulrc = DosQueryMem( start, &ulPageSize, &ulPageFlags);
    if (ulrc != NO_ERROR) {
        DPRINTF(( "tid#%d munmap pvdata=0x%08x DosQueryMem failed rc=%d\n", _gettid(), start, ulrc));
        return -1;
    }
    // if writable page, commit data before unmapping
    if ((ulPageFlags & PAG_WRITE) == PAG_WRITE) {
        ulrc = WtkUpdateMmf( mi.hmmf, start);
        if (ulrc != NO_ERROR) {
            DPRINTF(( "tid#%d munmap pvdata=0x%08x WtkUpdateMmf failed rc=%d\n", _gettid(), start, ulrc));
        }
    }

    // free the allocated file buffer
    ulrc = WtkFreeMmfFile( mi.hmmf, start);
    DPRINTF(( "tid#%d munmap: WtkFreeMmfFile hmmf 0x%08x, pvdata=0x%08x, rc=%u\n", _gettid(),
              mi.hmmf, start, ulrc));
    if (ulrc != NO_ERROR) {
        errno = EINVAL;
        return (int)MAP_FAILED;
    }

    // if no more file buffer allocated,
    // resources can be freed
    ulrc = WtkGetMmfInfo( &mi);
    if (mi.ulFiles == 0) {
        ulrc = WtkTerminateMmf( mi.hmmf);
        DPRINTF(( "tid#%d munmap: WtkTerminateMmf hmmf 0x%08x, rc=%u\n", _gettid(), mi.hmmf, ulrc));
    }

    return 0;
}

/* ------------------------------------------------------------ */

int msync(void *addr, size_t len, int flags)
{
    APIRET  ulrc;
    MMFINFO mi;

    // get handle to mmf
    ulrc = WtkGetMmfInfo( &mi);
    if (ulrc != NO_ERROR) {
        DPRINTF(( "PANIC in msync: MMF not initialized!\n"));
        exit(1);
    }

    // invalidate cache or update file
    if (flags & MS_INVALIDATE)
        ulrc = WtkRevertMmf( mi.hmmf, addr);
    else
        ulrc = WtkUpdateMmf( mi.hmmf, addr);

    DPRINTF(( "tid#%d msync %s hmmf 0x%08x, pvdata=0x%08x\n", _gettid(),
              ((flags & MS_INVALIDATE)) ?
                 "WtkRevertMmf" :
                 "WtkUpdateMmf",
              mi.hmmf,
              addr));

    if (ulrc != NO_ERROR)
        {
        errno = EINVAL;
        return (int)MAP_FAILED;
        }

   return 0;
}

/* ------------------------------------------------------------ */
#define PAG_SIZE 4096

int mcommit(void *pvdata, size_t length, int write)
{
   ULONG ulPageFlags = 0;
   ULONG ulPageSize = PAG_SIZE;
   MMFINFO mi;
   APIRET ulrc;
   int i;

   // get handle to mmf
   ulrc = WtkGetMmfInfo( &mi);
   if (ulrc != NO_ERROR) {
      DPRINTF(("WARNING in mcommit: MMF not initialized!\n"));
      return 0;
   }

   DPRINTF(( "tid#%d mcommit pvdata=0x%08x len=%d write=%d\n", _gettid(), pvdata, length, write));

   ulrc = WtkCommitMmf( mi.hmmf, pvdata, length);
   if (ulrc != NO_ERROR) {
      errno = EINVAL;
      return (int)MAP_FAILED;
   }

   return 0;
}
