#include <stdatomic.h>
#include <stddef.h>
#include <stdint.h>
#include <string.h>
#include <error.h>
#include <mm/vmm.h>
#include <mm/page.h>
#include <mm/slab.h>
#include <scheduler/sched.h>
#include <kprint.h>
#include <neobbo.h>
#include <lock.h>

struct ma_cache *caches = NULL;

atomic_flag caches_lock = ATOMIC_FLAG_INIT;


enum SLAB_STATE {
    FREE = 0,
    PARTIAL,
    USED
};


// Gets free object from slab and handles stuff
uint64_t *_ma_slab_get_free_obj(struct ma_slab *slab){
    if(slab->free == NULL){
        return NULL;
    }

    uint64_t *addr = slab->free->startaddr;

    if(addr == NULL){
        return NULL;
    }

    if(slab->free->next != NULL){

        slab->free = slab->free->next;
    }else{
        slab->free = NULL;
    }

    // Move the slab from the free to the partial list on the first time it's allocated from
    if(slab->refcount == 0){
        if(slab->prev != NULL){
            slab->prev->next = slab->next;
        }else{
            slab->cache->slabs_free = NULL;
        }

        /* If there is a partial slab head then make it the head and do other stuff */
        if(slab->cache->slabs_partial != NULL){
            slab->cache->slabs_partial->next = slab;
            slab->prev = slab->cache->slabs_partial;
        }
        
        slab->next = NULL;
        slab->cache->slabs_partial = slab;
    }

    slab->refcount++;

    return addr;

}

kstatus _ma_alloc_slab(struct ma_cache *kcache){
    struct ma_slab *slab_structure = (struct ma_slab*)va_alloc_contigious_pages(1);
    memset(slab_structure, 0, PAGE_SIZE);

    // Put the addresses in the slab structure into the bufctls
    if(kcache->objsize >= 512){
        /*  Here we store the bufctls seperately from the actual objects
        *
        */

        slab_structure->free = (struct ma_bufctl*)(va_alloc_contigious_pages(1)); // Store the bufctls off-page
        memset(slab_structure->free, 0, 4096);

        uint64_t slabsize = kcache->slabsize;
        void *mstart = va_alloc_contigious_pages(kcache->slabsize);

        for(size_t j = 0; j < kcache->slabsize; j++){
            get_page((void*)((uint64_t)mstart + j*PAGE_SIZE))->slab = slab_structure;
            get_page((void*)((uint64_t)mstart + j*PAGE_SIZE))->bufctls = slab_structure->free;
        }

        for(size_t i = 0; i < (PAGE_SIZE * slabsize)/kcache->objsize; i++){
            ((struct ma_bufctl*)((uint64_t)slab_structure->free + sizeof(struct ma_bufctl)*(i)))->startaddr = (size_t*)((uint64_t)mstart + i * kcache->objsize);
            ((struct ma_bufctl*)((uint64_t)slab_structure->free + sizeof(struct ma_bufctl)*(i)))->next = (struct ma_bufctl*)((uint64_t)slab_structure->free + sizeof(struct ma_bufctl)*(i+1));
        }
        
    }else{
        /* In this case the objects acts as bufctl structures. Small downside: there will always be a max of 252 objects per slab, no matter the size of the object, since
        *  they have to have enough space to store a bufctl structure (16 bytes).
        *
        *  Their startaddr is the same as the address of the bufctl, since the objects act as the bufctls
        */

        slab_structure->free = va_alloc_contigious_pages(kcache->slabsize);

        get_page(slab_structure->free)->slab = slab_structure;
        get_page(slab_structure->free)->bufctls = slab_structure->free;

        uint64_t size = (kcache->objsize >= sizeof(struct ma_bufctl)) ? kcache->objsize : sizeof(struct ma_bufctl);

        for(size_t i = 0; i < kcache->num; i++){
            ((struct ma_bufctl*)((uint64_t)slab_structure->free + size*i))->startaddr = (size_t*)((uint64_t)slab_structure->free + i * size);
            if(i+1 < kcache->num){
                ((struct ma_bufctl*)((uint64_t)slab_structure->free + size*i))->next = (struct ma_bufctl*)((uint64_t)slab_structure->free + size*(i+1));
            }else{
                ((struct ma_bufctl*)((uint64_t)slab_structure->free + size*i))->next = NULL;
            }
            
        }

        //memcpy(get_page(slab_structure->free)->bufctls, slab_structure->free, kcache->slabsize * PAGE_SIZE);
    }

    //asm("int $1");

    if(kcache->slabs_free == NULL){
        kcache->slabs_free = slab_structure;
    }else{
        // Change head
        kcache->slabs_free->next = slab_structure;
        slab_structure->prev = kcache->slabs_free;
        kcache->slabs_free = slab_structure;
    }
    
    slab_structure->cache = kcache;

    return KERNEL_STATUS_SUCCESS;

}
// TODO: fix this complicated POS
void _ma_move_slab(struct ma_slab *slab, enum SLAB_STATE newstate){
    struct ma_cache *cache = slab->cache;
    struct ma_slab *sb = 0;
    switch (newstate) {
        case FREE:
            if(cache->slabs_partial != NULL){
                sb = cache->slabs_partial;
                while(sb != NULL){
                    if(sb == slab){
                        goto free_common;
                    }
                    sb = sb->prev;
                }
            }

            if(cache->slabs_used != NULL){
                sb = cache->slabs_used;
                while(sb != NULL){
                    if(sb == slab){
                        goto free_common;
                    }
                    sb = sb->prev;
                }
            }
            return;
        case PARTIAL:
            if(cache->slabs_free != NULL){
                sb = cache->slabs_free;
                while(sb != NULL){
                    if(sb == slab){
                        goto partial_common;
                    }
                    sb = sb->prev;
                }
            }

            if(cache->slabs_used != NULL){
                sb = cache->slabs_used;
                while(sb != NULL){
                    if(sb == slab){
                        goto partial_common;
                    }
                    sb = sb->prev;
                }
            }
            return;
            
        case USED:
            if(cache->slabs_free != NULL){
                sb = cache->slabs_free;
                while(sb != NULL){
                    if(sb == slab){
                        goto used_common;
                    }
                    sb = sb->prev;
                }
            }

            if(cache->slabs_partial != NULL){
                sb = cache->slabs_partial;
                while(sb != NULL){
                    if(sb == slab){
                        goto used_common;
                    }
                    sb = sb->prev;
                }
            }
            return;
        }

    free_common:

        slab->next = NULL;
        slab->prev = NULL;

        // Preserve the linkage
        if(sb->prev != NULL){
            if(sb->next != NULL){
                sb->next->prev = sb->prev;
            }
            sb->prev->next = sb->next;
        }

        if(cache->slabs_free != NULL){
            cache->slabs_free->next = slab;
            slab->prev = cache->slabs_free;
        }

        cache->slabs_free = slab;

        return;
    partial_common:

        slab->next = NULL;
        slab->prev = NULL;

        if(sb->prev != NULL){
            if(sb->next != NULL){
                sb->next->prev = sb->prev;
            }
            sb->prev->next = sb->next;
        }

        if(cache->slabs_partial != NULL){
            cache->slabs_partial->next = slab;
            slab->prev = cache->slabs_partial;
        }

        cache->slabs_partial = slab;

        return;
    used_common:
        slab->next = NULL;
        slab->prev = NULL;

        if(sb->prev != NULL){
            if(sb->next != NULL){
                sb->next->prev = sb->prev;
            }
            sb->prev->next = sb->next;
        }

        if(cache->slabs_used != NULL){
            cache->slabs_used->next = slab;
            slab->prev = cache->slabs_used;
        }

        cache->slabs_used = slab;

        return;        
        
}

struct ma_cache *ma_cache_create(char *name, size_t size, uint32_t flags, void (*constructor)(void *, size_t), void (*destructor)(void *, size_t)){

    acquire_spinlock(&caches_lock);

    struct ma_cache *kcache = (struct ma_cache*)va_alloc_contigious_pages(1);
    memset(kcache, 0, 4096);

    memcpy(kcache->name, name, 16);
    kcache->slabsize = (size / PAGE_SIZE) + 1;
    kcache->num = (4096 * kcache->slabsize - sizeof(struct ma_slab)) / ((size >= sizeof(struct ma_bufctl)) ? size : sizeof(struct ma_bufctl)); // Calculate the number of buffers in this slab
    kcache->objsize = size;
    memset(&kcache->lock, 0, sizeof(atomic_flag));

    _ma_alloc_slab(kcache);

    if(caches != NULL){
        caches->next = kcache;
        kcache->prev = caches;
    }

    caches = kcache;

    free_spinlock(&caches_lock);

    return kcache;

}

void *ma_cache_alloc(struct ma_cache *kcache, uint32_t flags){

    acquire_spinlock(&kcache->lock);

    struct ma_slab *slab = NULL;

    if(kcache->slabs_free == NULL){
        if(kcache->slabs_partial == NULL){
            _ma_alloc_slab(kcache);
            slab = kcache->slabs_free;
        }else{
            slab = kcache->slabs_partial;
        }
    }else{
        slab = kcache->slabs_free;
    }

    uint64_t *addr = _ma_slab_get_free_obj(slab);

    // If there's no free object then allocate new slab
    if(addr == NULL){
        slab->free = NULL;

        if(kcache->slabs_partial->prev != NULL){
            kcache->slabs_partial = kcache->slabs_partial->prev;
        }else{
            kcache->slabs_partial = NULL;
        }

        if(kcache->slabs_used != NULL){
            kcache->slabs_used->next = slab;
            slab->prev = kcache->slabs_used;
            kcache->slabs_used = slab;
        }else{
            kcache->slabs_used = slab;
        }

        _ma_alloc_slab(kcache);
        addr = _ma_slab_get_free_obj(kcache->slabs_free); 
    }

    free_spinlock(&kcache->lock);

    return addr;

}

void cache_info(struct ma_cache *cache){
    kprintf("name: {s}\n", cache->name);
    kprintf("objsize: {d}\n", cache->objsize);
    kprintf("num: {d}\n", cache->num);
    kprintf("slabsize: {d}\n", cache->slabsize);

    int slabsfreecnt = 0;
    if(cache->slabs_free == NULL){
        kprintf("slabsfree: 0\n");
    }else{
        if(cache->slabs_free->prev == NULL){
            kprintf("slabsfree: 1\n");
        }else{
            struct ma_slab *slab = cache->slabs_free;

            while(slab->prev != NULL){
                slab = slab->prev;
                slabsfreecnt++;
            }

            kprintf("slabsfree : {d}\n", slabsfreecnt);
        }
    }

    int slabspartcnt = 0;
    if(cache->slabs_partial == NULL){
        kprintf("slabspartial: 0\n");
    }else{
        if(cache->slabs_partial->prev == NULL){
            kprintf("slabspartial: 1\n");
        }else{
            struct ma_slab *slab = cache->slabs_partial;
            while(slab->prev != NULL){
                slab = slab->prev;
                slabspartcnt++;
            }
            kprintf("slabspartial: {d}\n", slabspartcnt+1);
        }
    }

    int slabsfullcnt = 0;
    if(cache->slabs_used == NULL){
        kprintf("slabsused: 0\n");
    }else{
        if(cache->slabs_used->prev == NULL){
            kprintf("slabsused: 1\n");
        }else{
            struct ma_slab *slab = cache->slabs_used;
            while(slab->prev != NULL){
                slab = slab->prev;
                slabsfullcnt++;
            }
            

            kprintf("slabsused : {d}\n", slabsfullcnt);
        }
    }
}

struct ma_bufctl *addr_to_bufctl(void *object){
    struct ma_slab *slab = get_page(object)->slab;

    if(slab == NULL){
        return NULL;
    }

    if(slab->cache->objsize < 512){
        return (struct ma_bufctl*)object;
    }

    struct ma_bufctl *bufs = get_page(object)->bufctls; 

    if(bufs == NULL){
        return NULL;
    }

    for(size_t i = 0; i < slab->cache->num; i++){
        if((bufs + i)->startaddr != 0){
            kprintf("addr_to_bufctl: we're looking at 0x{x}\n", (bufs + i)->startaddr);
        }
        
        if((bufs + i)->startaddr == object){
            //kprintf("addr_to_bufctl: we're looking at 0x{x}\n", (bufs + i)->startaddr);
            return (bufs + i);
        }
    }

    return NULL;
}

kstatus ma_cache_dealloc(void *object){

    struct ma_slab *slab = get_page(object)->slab;

    if(slab == NULL){
        klog(__func__, "slab == null");
        return KERNEL_STATUS_ERROR;
    }

    acquire_spinlock(&slab->lock);

    struct ma_bufctl *buf = addr_to_bufctl(object);

    if(buf == NULL){
        klog(__func__, "bufctl not found");
        return KERNEL_STATUS_ERROR;
    }

    buf->next = slab->free;
    slab->free = buf;

    slab->refcount--;

    if(slab->refcount == slab->cache->num - 1){
        _ma_move_slab(slab, PARTIAL);
    }else if(slab->refcount == 0){
        _ma_move_slab(slab, FREE);
    }

    free_spinlock(&slab->lock);

    return KERNEL_STATUS_SUCCESS;

}