
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/mman.h>
#include <sys/uio.h>

#include "mod_cache.h"
#include "mmap_cache.h"

#include "http_protocol.h"
#include "http_request.h"
#include "http_main.h"
#include "http_log.h"

static int clock_pointer;
static ht_entry *hash_table;
static ctrl_block *cb_pool;

mmap_cache_status mc_stat;

static long mmap_cache_size;

static inline unsigned long cache_hash_key(char *s, int *len)
{
    /* s must be non-empty string and aligned as long */
    /* always returns 0 if len(s)<3 */
    unsigned long k;
    int i;
#define STEP sizeof(k)
    k = 0;                      /* the result will be here */
    for (i = 1; s[i]; i++)
        /* radix transform doesn't work well ?
           k = 256*k + s[i]; */
        if (i % STEP == 0)
            k ^= *(long *) (s + i - STEP);      /* XOR 'em all! */
    *len = i;
    return k;
#undef STEP
}

static inline int cache_strcmp(char *s1, int l1, char *s2, int l2)
{
    /* returns 0 if strings are equal, non-zero else */
    /* apply a simple heuristic */
    if (l1 != l2 || s1[l1 - 1] != s2[l1 - 1] || s1[l1 / 2] != s2[l1 / 2])
        return 1;
    return memcmp(s1, s2, l1);
}

void mmap_cache_init(int mcs)
{
    if (mcs <= 0)
        return;
    mmap_cache_size = mcs;
    if ((hash_table = calloc(mcs, sizeof(ht_entry))) == NULL
        || (cb_pool = calloc(mcs, sizeof(ctrl_block))) == NULL) {
        perror("calloc");
        exit(1);
    }
    fprintf(stderr, "Allocated %d bytes for mmap cache\n",
            mcs * (sizeof(ht_entry) + sizeof(ctrl_block)));
    mc_stat.req_cnt = mc_stat.hit_cnt = mc_stat.cached_files =
        mc_stat.cached_files_size = 0;
    clock_pointer = 0;
}

void mmap_cache_exit()
{
    free(hash_table);
    free(cb_pool);
    mmap_cache_size = 0;
}

/* remove control block cb from the hash table's overflow chain */
static inline void detach_cb(ctrl_block * cb)
{
    ctrl_block **cb_prev;
    ht_entry *hte = cb->hte;

    for (cb_prev = &(hte->cb); *cb_prev != cb; cb_prev = &((*cb_prev)->next))
        continue;
    *cb_prev = cb->next;        /* remove cb from chain */
}

/* get a free cb from the pool; or kill somebody using clock */
static ctrl_block *get_free_cb()
{
    ctrl_block *cb;

    while (cb_pool[clock_pointer].ref_counter-- > 0)
        clock_pointer = (clock_pointer + 1) % mmap_cache_size;
    cb = cb_pool + clock_pointer;
    clock_pointer = (clock_pointer + 1) % mmap_cache_size;
    if (cb->size == 0)          /* free block */
        return cb;
    detach_cb(cb);
    if (munmap(cb->addr, cb->size) == -1)
        /* not a fatal error hopefully */
        ap_log_error(APLOG_MARK, APLOG_ERR, 0 /*r->server */ ,
                    "munmap() failed in mmap_cache");
    mc_stat.cached_files--;
    mc_stat.cached_files_size -= cb->size;
    return cb;
}


/* hte is the hash table entry (overflow chain) where to insert the new cb.
   cb is the pointer to the new control block */
static int cache_request(request_rec * r, int fname_len, ht_entry * hte,
                         ctrl_block ** cbp)
{
    int fd;
    caddr_t addr;
    ctrl_block *cb;

    cb = 0;

    /* do not need pfopen() here (alarms are blocked) */
    fd = open(r->filename, O_RDONLY);
    if (fd == -1) {
        ap_log_error(APLOG_MARK, APLOG_ERR, r->server,
                    "file permissions deny server access: %s", r->filename);
        return FORBIDDEN;
    }

    addr = mmap(0, r->finfo.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
    close(fd);

    if (addr == (caddr_t) - 1) {
        ap_log_error(APLOG_MARK, APLOG_ERR, r->server,
                    "mmap_cache couldn't mmap: %s", r->filename);
        /* perhaps the default handler will be luckier */
        return DECLINED;
    }

    cb = cb_pool->next;

    mc_stat.cached_files++;
    mc_stat.cached_files_size += r->finfo.st_size;

    if (!cb)
        cb = get_free_cb(cb);

    cb->fname_len = fname_len;
    cb->size = r->finfo.st_size;
    cb->mtime = r->finfo.st_mtime;
    cb->ref_counter = 0;
    cb->hte = hte;
    cb->addr = addr;

    /* memcpy is faster than strcpy !? */
    memcpy(cb->fname, r->filename, fname_len + 1);

    /* insert cb in the hash table's overflow chain */
    cb->next = hte->cb;
    hte->cb = cb;
    *cbp = cb;
    return OK;
}

int mmap_cache_handle_request(request_rec * r, caddr_t * mmap_addr)
{
    ht_entry *hte;
    ctrl_block *cb;
    int error_status = 0;
    unsigned long hash_value;
    int fname_len;

    if (mmap_cache_size <= 0)
        return DECLINED;

    mc_stat.req_cnt++;

    /* fprintf(stderr, "Handling %s\n", r->filename); */
    /*
       if (mc_stat.req_cnt%5000 == 0)
       fprintf(stderr, "Req:%ld HR:%1.3f Files:%d  AvgSz:%ld\n", 
       mc_stat.req_cnt, (double)mc_stat.hit_cnt/mc_stat.req_cnt,
       mc_stat.cached_files, 
       mc_stat.cached_files_size/mc_stat.cached_files);
     */

    hash_value = cache_hash_key(r->filename, &fname_len);

    if (fname_len > MAX_FNAME_LENGTH) {
        /* fprintf(stderr, "too long a file name (%s)by (%d) bytes\n", r->filename, *fname_len-MAX_FNAME_LENGTH); */
        return DECLINED;
    }

    hte = hash_table + hash_value % mmap_cache_size;

    /* scan the overflow chain */
    cb = hte->cb;
    while (cb != NULL &&
        cache_strcmp(cb->fname, cb->fname_len, r->filename, fname_len) != 0)
        cb = cb->next;

    if (cb != NULL              /* found */
        && cb->mtime == r->finfo.st_mtime) {    /* valid copy */
        /* got a hit */
        mc_stat.hit_cnt++;

        if (cb->ref_counter < 100)
            cb->ref_counter++;

        r->finfo.st_size = cb->size;
    }
    else {                      /* got either a pure miss or an invalid copy */
        if (cb != NULL)         /* invalid copy */
            cb->ref_counter = -1;

        /* don't bother to remove the old copy; 
           it will be removed sooner or later anyway */

        error_status = cache_request(r, fname_len, hte, &cb);
    }

    *mmap_addr = (caddr_t) (cb ? cb->addr : 0);
    /* fprintf(stderr, "errstatus=%d addr=%p\n", error_status, *mmap_addr); */
    return error_status;
}
