#include "erl_nif.h"
#include <string.h>
#include "stdio.h"
#include "xxhash.h"

//#define DEBUG
#include "mcache.h"


ErlNifResourceType* mcache_t_handle;
static ERL_NIF_TERM atom_ok;
static ERL_NIF_TERM atom_undefined;
static ERL_NIF_TERM atom_overflow;


void print_entry(mc_entry_t *entry) {
  {};
  uint8_t i;
  if (!entry) {
    printf("\r\n");
    return;
  }
  printf("    @%ld:", entry->start);

  for (i=0; i < entry->count; i++) {
    printf(" %ld", entry->data[i]);
  };

  if (entry->next) {
    printf(" |");
    print_entry(entry->next);
  } else {
    printf("\r\n");
  };
  
}

void print_metric(mc_metric_t *metric) {
  uint16_t i;
  printf("  ");
  for(i = 0; i < metric->name_len; i++)
    printf("%c", (int) metric->name[i]);
  printf("[%zu]:\r\n", metric->alloc);
  print_entry(metric->head);
};

void init_buckets(mc_conf_t conf,/*@out@*/ mc_gen_t *gen) {
  dprint("init_buckets\r\n");
  gen->buckets = (mc_bucket_t *) mc_alloc(conf.buckets * sizeof(mc_bucket_t));
  for (int b = 0; b < conf.buckets; b++) {
    mc_bucket_t *bkt = &(gen->buckets[b]);

    for (int i = 0; i < LCOUNT; i++) {
      bkt->largest[i] = NULL;
    }

#ifdef TAGGED
    bkt->tag = TAG_BKT;
#endif

    for (int s = 0; s < SUBS; s++) {
      mc_sub_bucket_t *sub = &(bkt->subs[s]);
      sub->size = conf.initial_entries;
      sub->count = 0;
      sub->metrics = (mc_metric_t **) mc_alloc(sub->size * sizeof(mc_metric_t *));
#ifdef TAGGED
      sub->tag = TAG_SUB;
      for (int i = 0; i < sub->size; i++) {
        sub->metrics[i] = TAG_METRIC_L;
      }
#endif
    };
  }
}

void remove_largest(mc_bucket_t *bkt, mc_metric_t *metric) {
  int i = 0;
  dprint("[%p] remove largest: %llu\r\n",  bkt, metric->hash);
#ifdef DEBUG
  dprint("- >>");
  for(i = 0; i < LCOUNT; i++){
    if (! bkt->largest[i]) {
      break;
    };
    dprint(" %llu(%zu)", bkt->largest[i]->hash, bkt->largest[i]->alloc);
  };
  dprint("\r\n");
#endif
  
  i = 0;
  // short circuite when we have a last element
  if (bkt->largest[LCOUNT - 1]) {
    // if we are the last lement we can set it to null and return
    if (bkt->largest[LCOUNT - 1] == metric) {
      bkt->largest[LCOUNT - 1] = NULL;
      return;
    }
  }
  //skip all the ones we don't have
  while (i < LCOUNT && bkt->largest[i] != metric) {
    // if we found a null we can end our search
    if (!bkt->largest[i]) {
      dprint("return %d\r\n", i);
      return;
    }
    i++;
  };
  dprint("found %d\r\n", i);
  uint8_t moved = 0;
  while (i < LCOUNT - 1) {
    dprint("%d <- %d\r\n", i, i + 1);
    // found a null we don't need to go on.
    if (!bkt->largest[i]) {
      return;
    }
    moved = 1;
    bkt->largest[i] = bkt->largest[i+1];
    i++;
  }
  if (moved) {
    dprint("%d <- NULL\r\n", LCOUNT - 1);
    bkt->largest[LCOUNT - 1] = NULL;
  }
}

void insert_largest(mc_bucket_t *bkt, mc_metric_t *metric) {
  remove_largest(bkt, metric);
  dprint("[%p] insert largest: %llu\r\n", bkt, metric->hash);
#ifdef DEBUG
  dprint(">>");
  for(int j = 0; j < LCOUNT; j++){
    if (! bkt->largest[j]) {
      break;
    };
    dprint(" %llu(%zu)", bkt->largest[j]->hash, bkt->largest[j]->alloc);
  };
  dprint("\r\n");
#endif
  for (int i = 0; i < LCOUNT; i++) {
    if (!bkt->largest[i]) {
      dprint("[%d] <- %llu; %zu!\r\n", i, metric->hash, metric->alloc);
      bkt->largest[i] = metric;
      break;
    }
    if (metric == bkt->largest[i]) {
      break;
    }
    if (metric->alloc > bkt->largest[i]->alloc) {
      dprint("[%d] <- %llu: %zu\r\n", i, metric->hash, metric->alloc);
      mc_metric_t *t = bkt->largest[i];
      bkt->largest[i] = metric;
      metric = t;
    } else {
      dprint("[%d] | %llu: %zu\r\n", i, bkt->largest[i]->hash, bkt->largest[i]->alloc);
    }
  }
#ifdef DEBUG
  dprint("<<");
  for(int j = 0; j < LCOUNT; j++){
    if (! bkt->largest[j]) {
      break;
    };
    dprint(" %llu(%zu)", bkt->largest[j]->hash, bkt->largest[j]->alloc);
  };
  dprint("\r\n");
#endif

}

void age(mcache_t *cache) {
  dprint("age\r\n");
  for (int b = 0; b < cache->conf.buckets; b++) {
    // G1 -> G2
    for (int s = 0; s < SUBS; s++) {
      mc_sub_bucket_t *g2s = &(cache->g2.buckets[b].subs[s]);
      mc_sub_bucket_t *g1s = &(cache->g1.buckets[b].subs[s]);
      mc_metric_t **new_metrics = (mc_metric_t **) mc_alloc((g2s->count + g1s->count) * sizeof(mc_metric_t *));
      memcpy(new_metrics, g2s->metrics, g2s->count * sizeof(mc_metric_t *));
      mc_free(g2s->metrics);
      g2s->metrics = new_metrics;

      memcpy(
             // copy to the end of the current array
             &(g2s->metrics[g2s->count]),
             // copy the start ofn the lower gens array
             g1s->metrics,
             // Copy based on the sbuze of the old array
             g1s->count * sizeof(mc_metric_t *)
             );
      mc_free(g1s->metrics);
      g2s->count += g1s->count;
      g2s->size = g2s->count;
      // Free the G1 metric  list of thbs bucket as we copbed bt all out
    }
    // make sure to clear the largest
    for (int l = 0; l < LCOUNT; l++) {
      // move largest over when we have them
      if (cache->g1.buckets[b].largest[l]) {
        insert_largest(&(cache->g2.buckets[b]), cache->g1.buckets[b].largest[l]);
      } else {
        // we can stop our loop when we find the first null
        break;
      }
      // then set them to null
      cache->g1.buckets[b].largest[l] = NULL;
    }
  }
  // free g1 buckets (we copied the content to g2)
  mc_free(cache->g1.buckets);
  // move g0 buckets to g1
  cache->g1.buckets = cache->g0.buckets;

  cache->g2.alloc += cache->g1.alloc;
  cache->g1.alloc = cache->g0.alloc;
  cache->g0.alloc = 0;
  // reinitialize g0
  init_buckets(cache->conf, &(cache->g0));

}

static ERL_NIF_TERM serialize_entry(ErlNifEnv* env, mc_entry_t *entry) {
  ERL_NIF_TERM data;
  size_t to_copy = entry->count * sizeof(ErlNifUInt64);
  unsigned char *datap = enif_make_new_binary(env, to_copy, &data);
  memcpy(datap, entry->data, to_copy);
  return  enif_make_tuple2(env,
                           enif_make_uint64(env, entry->start),
                           data);

}

static ERL_NIF_TERM serialize_metric(ErlNifEnv* env, mc_metric_t *metric) {
  if (! metric) {
    return atom_undefined;
  }
  ERL_NIF_TERM result = enif_make_list(env, 0);
  ERL_NIF_TERM reverse;
  mc_entry_t *entry = metric->head;
  while (entry) {
    result = enif_make_list_cell(env, serialize_entry(env, entry), result);
    entry = entry->next;
  }
  enif_make_reverse_list(env, result, &reverse);
  return reverse;
}

static void free_entry(mc_entry_t *e) {
  if (e->next) {
    free_entry(e->next);
  }
  mc_free(e->data);
  mc_free(e);
}


static void free_metric(mc_metric_t *m) {
  if (m->head) {
    free_entry(m->head);
  }
  mc_free(m->name);
  mc_free(m);
}

static void free_gen(mc_conf_t conf, mc_gen_t gen) {
  for (int b = 0; b < conf.buckets; b++) {
    for (int s = 0; s < SUBS; s++) {
      for (int m = 0; m < gen.buckets[b].subs[s].count; m++) {
        free_metric(gen.buckets[b].subs[s].metrics[m]);
      };
      mc_free(gen.buckets[b].subs[s].metrics);
    }
  }
  mc_free(gen.buckets);
};

static void cache_dtor(ErlNifEnv* env, void* handle) {
  mcache_t * c = (mcache_t*) handle;
  free_gen(c->conf, c->g0);
  free_gen(c->conf, c->g1);
  free_gen(c->conf, c->g2);
};


static int
load(ErlNifEnv* env, void** priv, ERL_NIF_TERM load_info)
{
  ErlNifResourceFlags flags = (ErlNifResourceFlags)(ERL_NIF_RT_CREATE | ERL_NIF_RT_TAKEOVER);
  mcache_t_handle = enif_open_resource_type(env,
                                            "mcache_t",
                                            "cache",
                                            &cache_dtor,
                                            flags,
                                            0);
  atom_undefined = enif_make_atom(env, "undefined");
  atom_ok = enif_make_atom(env, "ok");
  atom_overflow = enif_make_atom(env, "overflow");
  return 0;
}

static int
upgrade(ErlNifEnv* env, void** priv, void** old_priv, ERL_NIF_TERM load_info)
{
  return 0;
}

static ERL_NIF_TERM
new_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {

  mcache_t *cache;
  ErlNifUInt64 max_alloc;
  ErlNifUInt64 buckets;
  ErlNifUInt64 age_cycle;
  ErlNifUInt64 initial_data_size;
  ErlNifUInt64 initial_entries;
  ErlNifUInt64 hash_seed;
  ErlNifUInt64 max_gap;
  if (argc != 7) {
    return enif_make_badarg(env);
  };
  if (!enif_get_uint64(env, argv[0], &max_alloc)) return enif_make_badarg(env);

  if (!enif_get_uint64(env, argv[1], &buckets)) return enif_make_badarg(env);
  if (buckets <= 0) return enif_make_badarg(env);

  if (!enif_get_uint64(env, argv[2], &age_cycle)) return enif_make_badarg(env);
  if (age_cycle <= 0) return enif_make_badarg(env);

  if (!enif_get_uint64(env, argv[3], &initial_data_size)) return enif_make_badarg(env);
  if (initial_data_size <= 0) return enif_make_badarg(env);

  if (!enif_get_uint64(env, argv[4], &initial_entries)) return enif_make_badarg(env);
  if (initial_entries <= 0) return enif_make_badarg(env);

  if (!enif_get_uint64(env, argv[5], &hash_seed)) return enif_make_badarg(env);
  if (hash_seed <= 0) return enif_make_badarg(env);

  if (!enif_get_uint64(env, argv[6], &max_gap)) return enif_make_badarg(env);

  cache = (mcache_t *) enif_alloc_resource(mcache_t_handle, sizeof(mcache_t));

#ifdef TAGGED
  cache->tag = TAG_CACHE;
#endif

  // Set up the config
  cache->conf.max_alloc = max_alloc;
  cache->conf.buckets = buckets;
  cache->conf.age_cycle = age_cycle;

  cache->conf.initial_data_size = initial_data_size;
  cache->conf.initial_entries = initial_entries;
  cache->conf.hash_seed = hash_seed;
  cache->conf.max_gap = max_gap;

  // some cache wqide counters
  cache->inserts = 0;
  cache->age = 0;

  //now set up the tree genreations
  cache->g0.v = 0;
  cache->g0.alloc = 0;
#ifdef TAGGED
  cache->g0.tag = TAG_GEN;
#endif

  init_buckets(cache->conf, &(cache->g0));
  cache->g1.v = 1;
  cache->g1.alloc = 0;
#ifdef TAGGED
  cache->g1.tag = TAG_GEN;
#endif
  init_buckets(cache->conf, &(cache->g1));
  cache->g2.v = 2;
  cache->g2.alloc = 0;
#ifdef TAGGED
  cache->g2.tag = TAG_GEN;
#endif
  init_buckets(cache->conf, &(cache->g2));
  ERL_NIF_TERM term = enif_make_resource(env, cache);
  enif_release_resource(cache);
  return term;
};


mc_metric_t *find_metric_g(mc_conf_t conf, mc_gen_t gen, uint64_t hash, uint16_t name_len, uint8_t *name) {
  // Itterate over the existig metrics and see if we have already
  // seen this one.
  uint64_t bucket = hash % conf.buckets;
  uint8_t sub = subid(hash);
  for (int i = 0; i < gen.buckets[bucket].subs[sub].count; i++) {
    mc_metric_t *m = gen.buckets[bucket].subs[sub].metrics[i];
    if (m->name_len == name_len
        && m->hash == hash
        && memcmp(m->name, name, name_len) == 0) {
      return m;
    }
  }
  return NULL;
};

mc_metric_t *find_metric(mcache_t *cache, uint64_t hash, uint16_t name_len, uint8_t *name) {
  mc_metric_t *res = find_metric_g(cache->conf, cache->g0, hash, name_len, name);
  if(!res) {
    res = find_metric_g(cache->conf, cache->g1, hash, name_len, name);
    if(!res) {
      res = find_metric_g(cache->conf, cache->g2, hash, name_len, name);
    }
  }
  return res;
}


mc_metric_t *find_metric_and_remove_g(mc_conf_t conf, mc_gen_t *gen, uint64_t hash, uint16_t name_len, uint8_t *name) {
  int i = 0;
  uint64_t bucket = hash % conf.buckets;
  uint8_t sub = subid(hash);

  // Itterate over the existig metrics and see if we have already
  // seen this one.

  for (i = 0; i < gen->buckets[bucket].subs[sub].count; i++) {
    mc_metric_t *m = gen->buckets[bucket].subs[sub].metrics[i];
    if (m->name_len == name_len
        && m->hash == hash
        && memcmp(m->name, name, name_len) == 0) {
      if (i != gen->buckets[bucket].subs[sub].count - 1) {
        gen->buckets[bucket].subs[sub].metrics[i] = gen->buckets[bucket].subs[sub].metrics[gen->buckets[bucket].subs[sub].count - 1];
      }
      remove_largest(&(gen->buckets[bucket]), m);
      gen->buckets[bucket].subs[sub].count--;
      gen->alloc -= m->alloc;
      return m;
    }
  }
  return NULL;
};

uint64_t remove_prefix_g(mc_conf_t conf, mc_gen_t *gen, uint16_t pfx_len, uint8_t *pfx) {
  int i = 0;
  uint64_t counter = 0;
  // Itterate over the existig metrics and see if we have already
  // seen this one.
  for (int bucket = 0; bucket < conf.buckets; bucket++) {
    for (int sub = 0 ; sub < SUBS; sub++) {
      for (i = 0; i < gen->buckets[bucket].subs[sub].count; i++) {
        mc_metric_t *m = gen->buckets[bucket].subs[sub].metrics[i];
        if (m->name_len >= pfx_len
            && memcmp(m->name, pfx, pfx_len) == 0) {
          if (i != gen->buckets[bucket].subs[sub].count - 1) {
            gen->buckets[bucket].subs[sub].metrics[i] = gen->buckets[bucket].subs[sub].metrics[gen->buckets[bucket].subs[sub].count - 1];
            // we chear we move the last element in the current position and then go a step
            // back so we re-do it
            i--;
          }
          gen->buckets[bucket].subs[sub].count--;
          gen->alloc -= m->alloc;
          remove_largest(&(gen->buckets[bucket]), m);
          free_metric(m);
          counter++;
        }
      }
    }
  }
  return counter;
};

uint64_t remove_prefix(mcache_t *cache, uint16_t pfx_len, uint8_t *pfx) {
  return remove_prefix_g(cache->conf, &(cache->g0), pfx_len, pfx) +
    remove_prefix_g(cache->conf, &(cache->g1), pfx_len, pfx) +
    remove_prefix_g(cache->conf, &(cache->g2), pfx_len, pfx);
}

mc_metric_t *find_metric_and_remove(mcache_t *cache, uint64_t hash, uint16_t name_len, uint8_t *name) {
  mc_metric_t *res = find_metric_and_remove_g(cache->conf, &(cache->g0), hash, name_len, name);
  if(!res) {
    res = find_metric_and_remove_g(cache->conf, &(cache->g1), hash, name_len, name);
    if(!res) {
      res = find_metric_and_remove_g(cache->conf, &(cache->g2), hash, name_len, name);
    };
  };
  return res;
}


mc_metric_t *get_metric(mcache_t *cache, uint64_t hash, uint16_t name_len, uint8_t *name) {

  uint64_t bucket = hash % cache->conf.buckets;
  uint8_t sub = subid(hash);
  // Itterate over the existig metrics and see if we have already
  // seen this one.
  mc_metric_t *metric = find_metric_g(cache->conf, cache->g0, hash, name_len, name);
  if (metric) {
    return metric;
  }
  mc_bucket_t *b = &(cache->g0.buckets[bucket]);
  mc_sub_bucket_t *s = &(b->subs[sub]);
  // We start with a small cache and grow it as required, so we need to
  // make sure the new index doesn't exceet the count. If it does
  // double the size of the cache.
  if (s->count >= s->size) {
    mc_metric_t **new_metrics = mc_alloc(s->size * 2 * sizeof(mc_metric_t *));
#ifdef TAGGED
    for (int i = 0; i < s->size * 2; i++) {
      new_metrics[i] = TAG_METRIC_L;
    }
#endif
    memcpy(new_metrics, s->metrics, s->count * sizeof(mc_metric_t *));
    mc_free(s->metrics);
    s->metrics = new_metrics;
    s->size = s->size * 2;
  }
  // we havn't seen the metric so we'll create a new one.
  metric = find_metric_and_remove_g(cache->conf, &(cache->g1), hash, name_len, name);
  if (!metric) {
    metric = find_metric_and_remove_g(cache->conf, &(cache->g2), hash, name_len, name);
  }
  if (!metric) {
    metric = (mc_metric_t *) mc_alloc(sizeof(mc_metric_t));
#ifdef TAGGED
    metric->tag = TAG_METRIC;
#endif

    metric->alloc = name_len + sizeof(mc_metric_t);
    metric->name = mc_alloc(name_len * sizeof(uint8_t));
    metric->hash = hash;
    metric->name_len = name_len;
    memcpy(metric->name, name, name_len);
    metric->head = NULL;
    metric->tail = NULL;
  }
  cache->g0.alloc += metric->alloc;
  s->metrics[s->count] = metric;
  s->count++;
  insert_largest(b, metric);
  return metric;
};

void add_point(mc_conf_t conf, mc_gen_t *gen, mc_metric_t *metric, ErlNifUInt64 offset, size_t count, ErlNifUInt64* values) {
  // If eitehr we have no data yet or the current data is larger then
  // the offset we generate a new metric.
  // In both cases next will be the current head given that next might
  // be empty and it's needed to set next to empty for the first element.
  mc_entry_t *entry = NULL;
  if((!metric->head) || offset < metric->head->start) {
    size_t alloc = sizeof(ErlNifUInt64) * MAX(conf.initial_data_size, count * 2);
    entry = mc_alloc(sizeof(mc_entry_t));
    entry->start = offset;
    entry->size = conf.initial_data_size;
    entry->data = (ErlNifUInt64 *) mc_alloc(alloc);

#ifdef TAGGED
    entry->tag = TAG_ENTRY;
    for (int i = 0; i < entry->size; i++) {
      entry->data[i] = TAG_DATA_L;
    }
#endif

    entry->count = 0;
    entry->next = metric->head;
    metric->head = entry;
    metric->tail = entry;
    metric->alloc += alloc + sizeof(mc_entry_t);
    gen->alloc += alloc + sizeof(mc_entry_t);
  }
  if (offset > metric->tail->start) {
    entry = metric->tail;
  } else {
    entry = metric->head;
  }
  do {
    // if the offset is beyond the next chunks start
    // just go ahead and go for this chunk
    if(entry->next && (offset >= entry->next->start)) {
      entry = entry->next;
      continue;
    }

    //We know that we either have no next chunk or the data is before
    //the start of the next chunk

    // we allocate a new chunk behind this and continue with that.
    uint64_t internal_offset = offset - entry->start;

    // or we'd have gaps
    dprint("CHECK: %llu > (%u + %llu)!\r\n", internal_offset, entry->count, conf.max_gap);
    if (internal_offset > (entry->count + conf.max_gap)) {
      dprint("we got a gap!\r\n");
      // TODO: we could combine that with merge and dertermin if we can
      // just pull the start back but that remains for another day
      mc_entry_t *next = mc_alloc(sizeof(mc_entry_t));
      // we allocate at least as much memory as we need for our data
      // so we don't need to allocate twice for bigger blocks
      next->size = MAX(conf.initial_data_size, count);
      uint64_t alloc = next->size * sizeof(ErlNifUInt64);
      // create the new entry with the given offset
      next->start = offset;
      // reserve the data
      next->data = (ErlNifUInt64 *) mc_alloc(alloc);

#ifdef TAGGED
      next->tag = TAG_ENTRY;
      for (int i = 0; i < next->size; i++) {
        next->data[i] = TAG_DATA_L;
      }
#endif

      // we have not put in data yet
      next->count = 0;

      // insert this inbetween this and the next element
      next->next = entry->next;
      entry->next = next;
      // if next->next is null then we know our next is now the tail
      if (!next->next) {
        metric->tail = next;
      }
      // we continue with this element as entry

      entry = next;


      metric->alloc += alloc + sizeof(mc_entry_t);
      gen->alloc += alloc + sizeof(mc_entry_t);

      continue;
    }

    // if we would overlap with the next chunk we combine the two
    if (entry->next &&
        offset + count + conf.max_gap >= entry->next->start) {
      // the new size is the delta between our start and the total end of the next chunk
      mc_entry_t *next = entry->next;
      uint64_t new_size = next->start + next->size - entry->start;
      uint64_t new_count = next->start - entry->start + next->count;

      dprint("Asked to write %lu -> %lu\r\n", offset, offset + count);
      dprint("combining %ld->%ld(%ld) and %ld->%ld(%ld)\r\n",
             entry->start, entry->start + entry->count, entry->start + entry->size,
             next->start, next->start + next->count, next->start + next->size);
      dprint("new range %ld->%llu(%lld)\r\n", entry->start, entry->start + new_count, entry->start + new_size);


      ErlNifUInt64 *new_data = (ErlNifUInt64 *) mc_alloc(new_size * sizeof(ErlNifUInt64));
#ifdef TAGGED
      for (int i = 0; i < new_size; i++) {
        new_data[i] = TAG_DATA_L;
      }
#endif


      // recalculate the allocation
      metric->alloc -= (entry->size * sizeof(ErlNifUInt64));
      gen->alloc -= (entry->size * sizeof(ErlNifUInt64));

      metric->alloc -= (next->size * sizeof(ErlNifUInt64));
      gen->alloc -= (next->size * sizeof(ErlNifUInt64));

      metric->alloc += new_size * sizeof(ErlNifUInt64);
      gen->alloc += new_size * sizeof(ErlNifUInt64);


      entry->next = next->next;
      // copy, free and reassign old data
      memcpy(new_data, entry->data, entry->count * sizeof(ErlNifUInt64));
      mc_free(entry->data);
      entry->data = new_data;
      // set new size and count
      entry->size = new_size;

      // now we calculate the delta of old and new start to get the offset in the
      // new array to copy data to then free it
      dprint("2nd chunk offset: %ld\r\n", next->start - entry->start);
      dprint("copying points: %ld\r\n", next->start - entry->start);

      dprint("Filling between %ld and %ld\r\n", entry->count, next->start - entry->start);
      for (int i = entry->count; i < next->start - entry->start; i++) {
        entry->data[i] = 0;
      }

      entry->count = new_count;

      memcpy(entry->data + next->start - entry->start, next->data, next->count* sizeof(ErlNifUInt64));
      mc_free(next->data);
      mc_free(next);
      // if we don't have a next we are now the tail!
      if (!entry->next) {
        metric->tail = entry;
      }
      // we go back to the start of the loop in case we'd overlap multiple ones (oh my!)
      continue;
    };

    // the offset in our data array

    if (internal_offset + count >= entry->size) {
      uint64_t new_size = entry->size * 2;
      // keep growing untill we're sure we have the right size
      while (internal_offset + count >= new_size) {
        new_size *= 2;
      }

      // prevent oddmath we just  removethe oldsizeand then add
      // the new size
      metric->alloc -= (entry->size * sizeof(ErlNifUInt64));
      gen->alloc -= (entry->size * sizeof(ErlNifUInt64));

      ErlNifUInt64 *new_data = (ErlNifUInt64 *) mc_alloc(new_size * sizeof(ErlNifUInt64));
#ifdef TAGGED
      for (int i = 0; i < new_size; i++) {
        new_data[i] = TAG_DATA_L;
      }
#endif

      memcpy(new_data, entry->data, entry->size * sizeof(ErlNifUInt64));
      mc_free(entry->data);
      entry->data = new_data;
      entry->size = new_size;
      metric->alloc += (entry->size * sizeof(ErlNifUInt64));
      gen->alloc += (entry->size * sizeof(ErlNifUInt64));
    }

    // fill gap with zeros
    for (int i = entry->count; i < internal_offset; i++) {
      entry->data[i] = 0;
    }

    memcpy(entry->data + internal_offset, values, count * sizeof(ErlNifUInt64));

    entry->count = MAX(offset - entry->start + count, entry->count);

    if (!entry->next) {
      metric->tail = entry;
    }
    return;
  } while (entry);
}

mc_metric_t *
check_limit(ErlNifEnv* env, mcache_t *cache, uint64_t max_alloc, uint64_t bucket) {
  if (max_alloc > cache->g0.alloc +
      cache->g1.alloc +
      cache->g2.alloc) {
    return NULL;
  }
  // If we don't have g2 entries we age so g1 becomes
  // g2
  mc_gen_t *gen = &(cache->g2);

  if (gen->alloc == 0) {
    gen = &(cache->g1);
  }

  // If we still have no g2 entries we aga again,
  // this way g0 effectively becomes g0
  if (gen->alloc == 0) {
    gen = &(cache->g0);
  }

  // if we still have no g2 entries we know it's
  // all for nothing and just give up
  if (gen->alloc == 0) {
    return NULL;
  }

  //we know we have at least 1 entrie so we grab the alst one
  // and reduce the count and reduce the alloc;

  //TODO: This is not good!
  mc_metric_t *metric = NULL;
  mc_sub_bucket_t *largest_sub =  NULL;
  mc_bucket_t *largest_bkt =  NULL;
  int largest_idx = 0;
  // try to find a metric using the largest cache first
  for (int b = 0; b < cache->conf.buckets; b++) {
    if (gen->buckets[b].largest[0] &&
        (!metric || metric->alloc < gen->buckets[b].largest[0]->alloc)) {
      largest_bkt = &(gen->buckets[b]);
      metric = largest_bkt->largest[0];
      largest_sub = &(largest_bkt->subs[subid(metric->hash)]);
    }
  }
  if (metric) {
    largest_idx = 0;
    remove_largest(largest_bkt, metric);
    // find the index of the metric
    while (largest_idx < SUBS && largest_sub->metrics[largest_idx] != metric) {
      largest_idx++;
    }
    // We didn't found anything that is problematic so we pretend it never
    // happend, the metric is already removed from the largest index
    if (largest_idx == SUBS) {
      dprint("Oh my\r\n");
      largest_idx = 0;
      metric = NULL;
      largest_bkt = NULL;
      largest_sub = NULL;
    }
  }
  for (int i = 0; i < cache->conf.buckets; i++) {
    // we break if we found a sutiable metric either in this loop or the loop before
    if (metric) {
      break;
    }

    // We itterate through all buckets starting after the bucket we just edited
    // that way we avoid always changing the same buket over and over
    int b = (i + bucket + 1) % cache->conf.buckets;
    mc_bucket_t *bkt = &(gen->buckets[b]);
    for (int sub = 0; sub < SUBS; sub++) {
      // If we found a non empty bucket we find the largest metric in there to
      // evict, that way we can can free up the 'most sensible' thing;
      for (int j = 0; j < bkt->subs[sub].count; j++) {
        if (!metric || bkt->subs[sub].metrics[j]->alloc >= metric->alloc) {
          largest_idx = j;
          largest_sub = &(bkt->subs[sub]);
          metric = largest_sub->metrics[j];
        }
      }
    }

  }
  if (metric) {
    if (largest_idx != largest_sub->count - 1) {
      largest_sub->metrics[largest_idx] = largest_sub->metrics[largest_sub->count - 1];
    }
    largest_sub->count--;
    gen->alloc -= metric->alloc;
    return metric;
  }
  return NULL;
  // now we work on exporting the metric
}

static ERL_NIF_TERM
insert_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
  mcache_t *cache;
  mc_metric_t *metric;
  ErlNifUInt64 offset;
  ErlNifBinary value;
  ErlNifBinary name_bin;

  ERL_NIF_TERM data;
  ERL_NIF_TERM name;
  unsigned char *namep;

  uint64_t bucket;
  if (argc != 4) {
    return enif_make_badarg(env);
  };
  if (!enif_get_resource(env, argv[0], mcache_t_handle, (void **)&cache)) {
    return enif_make_badarg(env);
  };
  if (!enif_inspect_binary(env, argv[1], &name_bin)) {
    return enif_make_badarg(env);
  };
  if (!enif_get_uint64(env, argv[2], &offset)) {
    return enif_make_badarg(env);
  };
  if (!enif_inspect_binary(env, argv[3], &value)) {
    return enif_make_badarg(env);
  };
  if (value.size % sizeof(ErlNifUInt64) && value.size >= 8) {
    return enif_make_badarg(env);
  }
  uint64_t hash = XXH64(name_bin.data, name_bin.size, cache->conf.hash_seed) ;
  bucket = hash % cache->conf.buckets;
  dprint("INSERT[%llu]: %llu %lu@%lu\r\n", bucket, hash, value.size / 8, offset);

  metric = get_metric(cache, hash, name_bin.size, name_bin.data);
  // Add the datapoint
  add_point(cache->conf, &(cache->g0), metric, offset, value.size / 8, (ErlNifUInt64 *) value.data);
  // update largest
  insert_largest(&(cache->g0.buckets[bucket]), metric);

  cache->inserts++;
  if (cache->inserts > cache->conf.age_cycle) {
    //age(cache);
    cache->age++;
    cache->inserts = 0;
  }
  // We now check for overflow note that metric is re-used here!
  metric = check_limit(env, cache, cache->conf.max_alloc, bucket);
  if (metric) {
    data = serialize_metric(env, metric);
    namep = enif_make_new_binary(env, metric->name_len, &name);
    memcpy(namep, metric->name, metric->name_len);
    free_metric(metric);
    return  enif_make_tuple3(env,
                             atom_overflow,
                             name,
                             data);
  }
  return atom_ok;
};

static ERL_NIF_TERM
pop_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
  mcache_t *cache;
  mc_metric_t *metric = NULL;
  ERL_NIF_TERM data;
  ERL_NIF_TERM name;
  unsigned char *namep;

  if (argc != 1) {
    return enif_make_badarg(env);
  };
  if (!enif_get_resource(env, argv[0], mcache_t_handle, (void **)&cache)) {
    return enif_make_badarg(env);
  };

  // We cheat here, we can create a 'pop' by just checkig for a 0 limit.
  metric = check_limit(env, cache, 0, 0);
  if (metric) {
    data = serialize_metric(env, metric);
    namep = enif_make_new_binary(env, metric->name_len, &name);
    memcpy(namep, metric->name, metric->name_len);
    free_metric(metric);
    return  enif_make_tuple3(env,
                             atom_ok,
                             name,
                             data);
  }
  return atom_undefined;

};

static ERL_NIF_TERM
get_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
  mcache_t *cache;
  mc_metric_t *metric;
  ErlNifBinary name_bin;
  if (argc != 2) {
    return enif_make_badarg(env);
  };
  if (!enif_get_resource(env, argv[0], mcache_t_handle, (void **)&cache)) {
    return enif_make_badarg(env);
  };

  if (!enif_inspect_binary(env, argv[1], &name_bin)) {
    return enif_make_badarg(env);
  };

  uint64_t hash = XXH64(name_bin.data, name_bin.size, cache->conf.hash_seed) ;
  metric = find_metric(cache, hash, name_bin.size, name_bin.data);
  if (metric) {
    return  enif_make_tuple2(env,
                             atom_ok,
                             serialize_metric(env, metric));
  } else {
    return atom_undefined;
  }
};

static ERL_NIF_TERM
take_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
  mcache_t *cache;
  mc_metric_t *metric;
  ErlNifBinary name_bin;
  if (argc != 2) {
    return enif_make_badarg(env);
  };
  if (!enif_get_resource(env, argv[0], mcache_t_handle, (void **)&cache)) {
    return enif_make_badarg(env);
  };

  if (!enif_inspect_binary(env, argv[1], &name_bin)) {
    return enif_make_badarg(env);
  };

  uint64_t hash = XXH64(name_bin.data, name_bin.size, cache->conf.hash_seed) ;
  metric = find_metric_and_remove(cache, hash, name_bin.size, name_bin.data);
  if (metric) {
    ERL_NIF_TERM res = enif_make_tuple2(env,
                                        atom_ok,
                                        serialize_metric(env, metric));
    free_metric(metric);
    return res;
  } else {
    return atom_undefined;
  }
};

static ERL_NIF_TERM
remove_prefix_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
  mcache_t *cache;
  ErlNifBinary pfx_bin;
  if (argc != 2) {
    return enif_make_badarg(env);
  };
  if (!enif_get_resource(env, argv[0], mcache_t_handle, (void **)&cache)) {
    return enif_make_badarg(env);
  };

  if (!enif_inspect_binary(env, argv[1], &pfx_bin)) {
    return enif_make_badarg(env);
  };

  uint64_t count = remove_prefix(cache, pfx_bin.size, pfx_bin.data);
  return  enif_make_tuple2(env,
                           atom_ok,
                           enif_make_uint64(env, count));
};


void print_gen(mc_conf_t conf, mc_gen_t gen) {
  int size = 0;
  int count = 0;
  for (int i = 0; i < conf.buckets; i++) {
    for (int sub = 0; sub < SUBS; sub++) {
      size += gen.buckets[i].subs[sub].size;
      count += gen.buckets[i].subs[sub].count;
    }
  };
  printf("Cache: [c: %d |s: %d|a: %zu]:\r\n",  count, size, gen.alloc);
  for (int i = 0; i < conf.buckets; i++) {
    for (int sub = 0; sub < SUBS; sub++) {
      for(int j = 0; j < gen.buckets[i].subs[sub].count; j++) {
        print_metric(gen.buckets[i].subs[sub].metrics[j]);
      }
    }
  }
}

static ERL_NIF_TERM
print_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
  mcache_t *cache;
  if (argc != 1) {
    return enif_make_badarg(env);
  };
  if (!enif_get_resource(env, argv[0], mcache_t_handle, (void **)&cache)) {
    return enif_make_badarg(env);
  };
  print_gen(cache->conf, cache->g0);
  print_gen(cache->conf, cache->g1);
  print_gen(cache->conf, cache->g2);
  fflush(stdout);
  return  atom_ok;
};
static ERL_NIF_TERM
age_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
  mcache_t *cache;
  if (argc != 1) {
    return enif_make_badarg(env);
  };
  if (!enif_get_resource(env, argv[0], mcache_t_handle, (void **)&cache)) {
    return enif_make_badarg(env);
  };
  age(cache);
  return  atom_ok;
};

static ERL_NIF_TERM
gen_stats(ErlNifEnv* env, mc_conf_t conf, mc_gen_t gen) {
  int size = 0;
  int count = 0;
  for (int i = 0; i < conf.buckets; i++) {
    for (int sub = 0; sub < SUBS; sub++) {
      size += gen.buckets[i].subs[sub].size;
      count += gen.buckets[i].subs[sub].count;
    }
  };

  return enif_make_list3(env,
                         enif_make_tuple2(env,
                                          enif_make_atom(env, "alloc"),
                                          enif_make_uint64(env, gen.alloc)),
                         enif_make_tuple2(env,
                                          enif_make_atom(env, "count"),
                                          enif_make_uint64(env, count)),
                         enif_make_tuple2(env,
                                          enif_make_atom(env, "size"),
                                          enif_make_uint64(env, size)));

}

static ERL_NIF_TERM
conf_info(ErlNifEnv* env, mc_conf_t conf) {
  return enif_make_list5(env,
                         enif_make_tuple2(env,
                                          enif_make_atom(env, "buckets"),
                                          enif_make_uint64(env, conf.buckets)),
                         enif_make_tuple2(env,
                                          enif_make_atom(env, "age_cycle"),
                                          enif_make_uint64(env, conf.age_cycle)),
                         enif_make_tuple2(env,
                                          enif_make_atom(env, "initial_data_size"),
                                          enif_make_uint64(env, conf.initial_data_size)),
                         enif_make_tuple2(env,
                                          enif_make_atom(env, "initial_entries"),
                                          enif_make_uint64(env, conf.initial_entries)),
                         enif_make_tuple2(env,
                                          enif_make_atom(env, "max_alloc"),
                                          enif_make_uint64(env, conf.max_alloc)));
}



static ERL_NIF_TERM
stats_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
  mcache_t *cache;
  if (argc != 1) {
    return enif_make_badarg(env);
  };
  if (!enif_get_resource(env, argv[0], mcache_t_handle, (void **)&cache)) {
    return enif_make_badarg(env);
  };

  return enif_make_list7(env,
                         enif_make_tuple2(env,
                                          enif_make_atom(env, "age"),
                                          enif_make_uint64(env, cache->age)),
                         enif_make_tuple2(env,
                                          enif_make_atom(env, "inserts"),
                                          enif_make_uint64(env, cache->inserts)),
                         enif_make_tuple2(env,
                                          enif_make_atom(env, "conf"),
                                          conf_info(env, cache->conf)),
                         enif_make_tuple2(env,
                                          enif_make_atom(env, "total_alloc"),
                                          enif_make_uint64(env,
                                                           cache->g0.alloc +
                                                           cache->g1.alloc +
                                                           cache->g2.alloc)),
                         enif_make_tuple2(env,
                                          enif_make_atom(env, "gen0"),
                                          gen_stats(env, cache->conf, cache->g0)),
                         enif_make_tuple2(env,
                                          enif_make_atom(env, "gen1"),
                                          gen_stats(env, cache->conf, cache->g1)),
                         enif_make_tuple2(env,
                                          enif_make_atom(env, "gen2"),
                                          gen_stats(env, cache->conf, cache->g2)));
};
static ERL_NIF_TERM
is_empty_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
  mcache_t *cache;
  if (argc != 1) {
    return enif_make_badarg(env);
  };
  if (!enif_get_resource(env, argv[0], mcache_t_handle, (void **)&cache)) {
    return enif_make_badarg(env);
  };
  if (cache->g0.alloc == 0 && cache->g1.alloc == 0 && cache->g2.alloc == 0) {
    return enif_make_atom(env, "true");
  } else {
    return enif_make_atom(env, "false");
  }
};

static ErlNifFunc nif_funcs[] = {
  {"new", 7, new_nif},
  {"print", 1, print_nif},
  {"stats", 1, stats_nif},
  {"is_empty", 1, is_empty_nif},
  {"age", 1, age_nif},
  {"pop", 1, pop_nif},
  {"remove_prefix", 2, remove_prefix_nif},
  {"get", 2, get_nif},
  {"take", 2, take_nif},
  {"insert", 4, insert_nif},
};

// Initialize this NIF library.
//
// Args: (MODULE, ErlNifFunc funcs[], load, reload, upgrade, unload)
// Docs: http://erlang.org/doc/man/erl_nif.html#ERL_NIF_INIT

ERL_NIF_INIT(mcache, nif_funcs, &load, NULL, &upgrade, NULL);

/*
  {ok, H} = mcache:new(50*10*8).
  mcache:insert(H, <<"1">>,  1, <<1:64>>).
  mcache:insert(H, <<"2">>,  1, <<1:64>>).
  mcache:insert(H, <<"3">>,  1, <<1:64>>).
  mcache:insert(H, <<"4">>,  1, <<1:64>>).
  mcache:insert(H, <<"5">>,  1, <<1:64>>).
  mcache:insert(H, <<"6">>,  1, <<1:64>>).
  mcache:insert(H, <<"7">>,  1, <<1:64>>).
  mcache:insert(H, <<"8">>,  1, <<1:64>>).
  mcache:insert(H, <<"9">>,  1, <<1:64>>).
  mcache:age(H).

  mcache:insert(H, <<"test2">>, 1, <<2:64>>).
  mcache:age(H).
  mcache:insert(H, <<"test">>,  6, <<3:64>>).
  mcache:print(H).

  mcache:get(H, <<"test">>).

  mcache:insert(H, <<"test1">>, 1, <<2:64>>).
  mcache:insert(H, <<"test2">>, 1, <<2:64>>).
  mcache:insert(H, <<"test3">>, 1, <<2:64>>).
  mcache:insert(H, <<"test4">>, 1, <<2:64>>).
  mcache:insert(H, <<"test5">>, 1, <<2:64>>).
  mcache:insert(H, <<"test6">>, 1, <<2:64>>).
  mcache:insert(H, <<"test7">>, 1, <<2:64>>).
  mcache:insert(H, <<"test8">>, 1, <<2:64>>).
  mcache:insert(H, <<"test9">>, 1, <<2:64>>).
  mcache:insert(H, <<"testa">>, 1, <<2:64>>).
  mcache:insert(H, <<"testb">>, 1, <<2:64>>).
  mcache:insert(H, <<"testc">>, 1, <<2:64>>).
  mcache:get(H, <<"test">>).
*/


/*
  {ok, H} = mcache:new(10000).
  mcache:insert(H, <<"test">>,  1, <<2:64>>).
  mcache:age(H).
  mcache:age(H).
  mcache:insert(H, <<"test1">>,  2, <<2:64>>).
  mcache:print(H).
  mcache:age(H).
*/

/*
  {ok, H} =  mcache:new(726).
  mcache:insert(H, <<>>, 0, <<0,0,0,0,0,0,0,0>>).
  mcache:print(H).
  mcache:age(H).
  mcache:print(H).
  mcache:insert(H, <<>>, 0, <<0,0,0,0,0,0,0,0>>).
  mcache:print(H).
*/
