/*
 * Copyright (C) 2006 Evgeniy Stepanov <eugeni.stepanov@gmail.com>
 * Copyright (C) 2011 Grigori Goronzy <greg@chown.ath.cx>
 *
 * This file is part of libass.
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

#include "config.h"
#include "ass_compat.h"

#include <inttypes.h>
#include <ft2build.h>
#include FT_OUTLINE_H
#include <assert.h>

#include "ass_utils.h"
#include "ass_font.h"
#include "ass_cache.h"

// type-specific functions
// create hash/compare functions for bitmap, outline and composite cache
#define CREATE_HASH_FUNCTIONS
#include "ass_cache_template.h"
#define CREATE_COMPARISON_FUNCTIONS
#include "ass_cache_template.h"

// font cache
static unsigned font_hash(void *buf, size_t len)
{
    ASS_FontDesc *desc = buf;
    unsigned hval;
    hval = fnv_32a_str(desc->family, FNV1_32A_INIT);
    hval = fnv_32a_buf(&desc->bold, sizeof(desc->bold), hval);
    hval = fnv_32a_buf(&desc->italic, sizeof(desc->italic), hval);
    hval = fnv_32a_buf(&desc->vertical, sizeof(desc->vertical), hval);
    return hval;
}

static unsigned font_compare(void *key1, void *key2, size_t key_size)
{
    ASS_FontDesc *a = key1;
    ASS_FontDesc *b = key2;
    if (strcmp(a->family, b->family) != 0)
        return 0;
    if (a->bold != b->bold)
        return 0;
    if (a->italic != b->italic)
        return 0;
    if (a->vertical != b->vertical)
        return 0;
    return 1;
}

static void font_destruct(void *key, void *value)
{
    ass_font_free(value);
    free(key);
}

// bitmap cache
static void bitmap_destruct(void *key, void *value)
{
    BitmapHashValue *v = value;
    BitmapHashKey *k = key;
    if (v->bm)
        ass_free_bitmap(v->bm);
    if (v->bm_o)
        ass_free_bitmap(v->bm_o);
    if (k->type == BITMAP_CLIP)
        free(k->u.clip.text);
    free(key);
    free(value);
}

static size_t bitmap_size(void *value, size_t value_size)
{
    BitmapHashValue *val = value;
    size_t size = sizeof(BitmapHashKey) + sizeof(BitmapHashValue);
    if (val->bm)
        size += sizeof(Bitmap) + val->bm->stride * val->bm->h;
    if (val->bm_o)
        size += sizeof(Bitmap) + val->bm_o->stride * val->bm_o->h;
    return size;
}

static unsigned bitmap_hash(void *key, size_t key_size)
{
    BitmapHashKey *k = key;
    switch (k->type) {
        case BITMAP_OUTLINE: return outline_bitmap_hash(&k->u, key_size);
        case BITMAP_CLIP: return clip_bitmap_hash(&k->u, key_size);
        default: return 0;
    }
}

static unsigned bitmap_compare (void *a, void *b, size_t key_size)
{
    BitmapHashKey *ak = a;
    BitmapHashKey *bk = b;
    if (ak->type != bk->type) return 0;
    switch (ak->type) {
        case BITMAP_OUTLINE: return outline_bitmap_compare(&ak->u, &bk->u, key_size);
        case BITMAP_CLIP: return clip_bitmap_compare(&ak->u, &bk->u, key_size);
        default: return 0;
    }
}

// composite cache
static void composite_destruct(void *key, void *value)
{
    CompositeHashValue *v = value;
    CompositeHashKey *k = key;
    if (v->bm)
        ass_free_bitmap(v->bm);
    if (v->bm_o)
        ass_free_bitmap(v->bm_o);
    if (v->bm_s)
        ass_free_bitmap(v->bm_s);
    free(k->bitmaps);
    free(key);
    free(value);
}

static size_t composite_size(void *value, size_t value_size)
{
    CompositeHashValue *val = value;
    size_t size = sizeof(CompositeHashKey) + sizeof(CompositeHashValue);
    if (val->bm)
        size += sizeof(Bitmap) + val->bm->stride * val->bm->h;
    if (val->bm_o)
        size += sizeof(Bitmap) + val->bm_o->stride * val->bm_o->h;
    if (val->bm_s)
        size += sizeof(Bitmap) + val->bm_s->stride * val->bm_s->h;
    return size;
}

static unsigned composite_hash(void *key, size_t key_size)
{
    CompositeHashKey *k = key;
    unsigned hval = filter_hash(&k->filter, key_size);
    for (size_t i = 0; i < k->bitmap_count; ++i) {
        hval = fnv_32a_buf(&k->bitmaps[i].image, sizeof(k->bitmaps[i].image), hval);
        hval = fnv_32a_buf(&k->bitmaps[i].x, sizeof(k->bitmaps[i].x), hval);
        hval = fnv_32a_buf(&k->bitmaps[i].y, sizeof(k->bitmaps[i].y), hval);
    }
    return hval;
}

static unsigned composite_compare(void *a, void *b, size_t key_size)
{
    CompositeHashKey *ak = a;
    CompositeHashKey *bk = b;
    if (ak->bitmap_count != bk->bitmap_count)
        return 0;
    for (size_t i = 0; i < ak->bitmap_count; ++i) {
        if (ak->bitmaps[i].image != bk->bitmaps[i].image ||
            ak->bitmaps[i].x != bk->bitmaps[i].x ||
            ak->bitmaps[i].y != bk->bitmaps[i].y)
            return 0;
    }
    return filter_compare(&ak->filter, &bk->filter, key_size);
}

// outline cache

static unsigned outline_hash(void *key, size_t key_size)
{
    OutlineHashKey *k = key;
    switch (k->type) {
        case OUTLINE_GLYPH: return glyph_hash(&k->u, key_size);
        case OUTLINE_DRAWING: return drawing_hash(&k->u, key_size);
        default: return 0;
    }
}

static unsigned outline_compare(void *a, void *b, size_t key_size)
{
    OutlineHashKey *ak = a;
    OutlineHashKey *bk = b;
    if (ak->type != bk->type) return 0;
    switch (ak->type) {
        case OUTLINE_GLYPH: return glyph_compare(&ak->u, &bk->u, key_size);
        case OUTLINE_DRAWING: return drawing_compare(&ak->u, &bk->u, key_size);
        default: return 0;
    }
}

static void outline_destruct(void *key, void *value)
{
    OutlineHashValue *v = value;
    OutlineHashKey *k = key;
    outline_free(v->outline);
    free(v->outline);
    outline_free(v->border);
    free(v->border);
    if (k->type == OUTLINE_DRAWING)
        free(k->u.drawing.text);
    free(key);
    free(value);
}



// Cache data
typedef struct cache_item {
    void *key;
    void *value;
    struct cache_item *next;
} CacheItem;

struct cache {
    unsigned buckets;
    CacheItem **map;

    HashFunction hash_func;
    ItemSize size_func;
    HashCompare compare_func;
    CacheItemDestructor destruct_func;
    size_t key_size;
    size_t value_size;

    size_t cache_size;
    unsigned hits;
    unsigned misses;
    unsigned items;
};

// Hash for a simple (single value or array) type
static unsigned hash_simple(void *key, size_t key_size)
{
    return fnv_32a_buf(key, key_size, FNV1_32A_INIT);
}

// Comparison of a simple type
static unsigned compare_simple(void *a, void *b, size_t key_size)
{
    return memcmp(a, b, key_size) == 0;
}

// Default destructor
static void destruct_simple(void *key, void *value)
{
    free(key);
    free(value);
}


// Create a cache with type-specific hash/compare/destruct/size functions
Cache *ass_cache_create(HashFunction hash_func, HashCompare compare_func,
                        CacheItemDestructor destruct_func, ItemSize size_func,
                        size_t key_size, size_t value_size)
{
    Cache *cache = calloc(1, sizeof(*cache));
    if (!cache)
        return NULL;
    cache->buckets = 0xFFFF;
    cache->hash_func = hash_simple;
    cache->compare_func = compare_simple;
    cache->destruct_func = destruct_simple;
    cache->size_func = size_func;
    if (hash_func)
        cache->hash_func = hash_func;
    if (compare_func)
        cache->compare_func = compare_func;
    if (destruct_func)
        cache->destruct_func = destruct_func;
    cache->key_size = key_size;
    cache->value_size = value_size;
    cache->map = calloc(cache->buckets, sizeof(CacheItem *));
    if (!cache->map) {
        free(cache);
        return NULL;
    }

    return cache;
}

void *ass_cache_put(Cache *cache, void *key, void *value)
{
    unsigned bucket = cache->hash_func(key, cache->key_size) % cache->buckets;
    CacheItem **bucketptr = &cache->map[bucket];

    CacheItem *item = calloc(1, sizeof(CacheItem));
    if (!item)
        return NULL;
    item->key = malloc(cache->key_size);
    item->value = malloc(cache->value_size);
    if (!item->key || !item->value) {
        free(item->key);
        free(item->value);
        free(item);
        return NULL;
    }
    memcpy(item->key, key, cache->key_size);
    memcpy(item->value, value, cache->value_size);

    item->next = *bucketptr;
    *bucketptr = item;

    cache->items++;
    if (cache->size_func)
        cache->cache_size += cache->size_func(value, cache->value_size);
    else
        cache->cache_size++;

    return item->value;
}

void *ass_cache_get(Cache *cache, void *key)
{
    unsigned bucket = cache->hash_func(key, cache->key_size) % cache->buckets;
    CacheItem *item = cache->map[bucket];
    while (item) {
        if (cache->compare_func(key, item->key, cache->key_size)) {
            cache->hits++;
            return item->value;
        }
        item = item->next;
    }
    cache->misses++;
    return NULL;
}

int ass_cache_empty(Cache *cache, size_t max_size)
{
    int i;

    if (cache->cache_size < max_size)
        return 0;

    for (i = 0; i < cache->buckets; i++) {
        CacheItem *item = cache->map[i];
        while (item) {
            CacheItem *next = item->next;
            cache->destruct_func(item->key, item->value);
            free(item);
            item = next;
        }
        cache->map[i] = NULL;
    }

    cache->items = cache->hits = cache->misses = cache->cache_size = 0;

    return 1;
}

void ass_cache_stats(Cache *cache, size_t *size, unsigned *hits,
                     unsigned *misses, unsigned *count)
{
    if (size)
        *size = cache->cache_size;
    if (hits)
        *hits = cache->hits;
    if (misses)
        *misses = cache->misses;
    if (count)
        *count = cache->items;
}

void ass_cache_done(Cache *cache)
{
    ass_cache_empty(cache, 0);
    free(cache->map);
    free(cache);
}

// Type-specific creation function
Cache *ass_font_cache_create(void)
{
    return ass_cache_create(font_hash, font_compare, font_destruct,
            (ItemSize)NULL, sizeof(ASS_FontDesc), sizeof(ASS_Font));
}

Cache *ass_outline_cache_create(void)
{
    return ass_cache_create(outline_hash, outline_compare, outline_destruct,
            NULL, sizeof(OutlineHashKey), sizeof(OutlineHashValue));
}

Cache *ass_glyph_metrics_cache_create(void)
{
    return ass_cache_create(glyph_metrics_hash, glyph_metrics_compare, NULL,
            (ItemSize) NULL, sizeof(GlyphMetricsHashKey),
            sizeof(GlyphMetricsHashValue));
}

Cache *ass_bitmap_cache_create(void)
{
    return ass_cache_create(bitmap_hash, bitmap_compare, bitmap_destruct,
            bitmap_size, sizeof(BitmapHashKey), sizeof(BitmapHashValue));
}

Cache *ass_composite_cache_create(void)
{
    return ass_cache_create(composite_hash, composite_compare,
            composite_destruct, composite_size, sizeof(CompositeHashKey),
            sizeof(CompositeHashValue));
}
