#include <assert.h>
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include <stdbool.h>

#define RED_START "\033[31m"
#define COLOR_END "\033[0m"

#define DEBUG

typedef enum { dm, fa } cache_map_t;
typedef enum { uc, sc } cache_org_t;
typedef enum { instruction, data } access_t;

typedef struct {
  uint32_t address;
  access_t accesstype;
} mem_access_t;

typedef struct {
  uint64_t accesses;
  uint64_t hits;
  // You can declare additional statistics if
  // you like, however you are now allowed to
  // remove the accesses or hits
} cache_stat_t;

// DECLARE CACHES AND COUNTERS FOR THE STATS HERE

typedef struct {
  bool is_initialized;
  int content;
} cache_block_t;

cache_block_t* cache;

uint32_t cache_size;
uint32_t block_size = 64;
cache_map_t cache_mapping;
cache_org_t cache_org;

// Misc

// USE THIS FOR YOUR CACHE STATISTICS
cache_stat_t cache_statistics;

/* Reads a memory access from the trace file and returns
 * 1) access type (instruction or data access
 * 2) memory address
 */
mem_access_t read_transaction(FILE* ptr_file) {
  char buf[1000];
  char* token;
  char* string = buf;
  mem_access_t access;

  if (fgets(buf, 1000, ptr_file) != NULL) {
    /* Get the access type */
    token = strsep(&string, " \n");
    if (strcmp(token, "I") == 0) {
      access.accesstype = instruction;
    } else if (strcmp(token, "D") == 0) {
      access.accesstype = data;
    } else {
      printf(
          "Could not parse access type:\n"
          "%s%s%s %s\n",
          RED_START, token, COLOR_END, strsep(&string, " \n"));
      exit(0);
    }

    /* Get the access type */
    token = strsep(&string, " \n");
    access.address = (uint32_t)strtol(token, NULL, 16);

    return access;
  }

  /* If there are no more entries in the file,
   * return an address 0 that will terminate the infinite loop in main
   */
  access.address = 0;
  return access;
}

static int cache_access(mem_access_t access) {

}

const char* usage =
  "Usage:\n"
  "cache_sim size mapping organization [file]\n"
  "\tsize: 128-4096\n"
  "\tmapping: dm | fa \n"
  "\torganization: uc | sc\n"
  "\tfile: path\n";

/* Read command-line parameters and initialize:
 * cache_size, cache_mapping and cache_org variables
 */
static void handle_arguments(int argc, char** argv) {
  if (argc < 4) perror(usage);
  /* argv[0] is program name, parameters start with argv[1] */

  /* Set cache size */
  cache_size = atoi(argv[1]);
  // if (128 < cache_size || cache_size < 4096) error(
  //     "Cache size needs to be be between 128 and 4096\n"
  //     "Please check the input: %s%s%s",
  //     RED_START, argv[1], COLOR_END
  // );

  /* Set Cache Mapping */
  if (strcmp(argv[2], "dm") == 0) {
    cache_mapping = dm;
  } else if (strcmp(argv[2], "fa") == 0) {
    cache_mapping = fa;
  } else {
    printf("Unknown cache mapping\n");
    exit(1);
  }

  /* Set Cache Organization */
  if (strcmp(argv[3], "uc") == 0) {
    cache_org = uc;
  } else if (strcmp(argv[3], "sc") == 0) {
    cache_org = sc;
  } else {
    printf("Unknown cache organization\n");
    exit(1);
  }
}

static bool access_cache_dm(int index, int tag) {
  bool foundMatch = cache[index].is_initialized
                 && cache[index].content == tag;

  if (foundMatch) return true;

  cache[index].is_initialized = true;
  cache[index].content = tag;

  #ifdef DEBUG
    // printf("Overwriting tag at cache[%d] = %x\n", index, cache + index);
  #endif

  return false;
}

static bool access_cache_fa(int tag, cache_block_t* local_cache, int* counter, int number_of_blocks) {
  for (int i = 0; i < number_of_blocks; i++)
    if (local_cache[i].is_initialized && local_cache[i].content == tag)
      return true;

  (*counter)++;
  // if (*counter == number_of_blocks) *counter = 0;
  (*counter) %= number_of_blocks;
  local_cache[*counter].is_initialized = true;
  local_cache[*counter].content = tag;

  #ifdef DEBUG
    // printf("Overwriting tag at cache[%d] = %x\n", *counter, local_cache + *counter);
  #endif

  return false;
}

int main(int argc, char** argv) {
  // Reset statistics:
  memset(&cache_statistics, 0, sizeof(cache_stat_t));

  handle_arguments(argc, argv);

  // Will truncate 0, but that should not matter if args are handled correctly.
  int number_of_blocks = cache_size / block_size;

  int bits_for_offset = 0;
  int bs = block_size;
  while (bs % 2 == 0) {
    bits_for_offset++;
    bs = bs >> 1;
  }

  int bits_for_index = 0;
  if (cache_mapping == dm) {
    int nob = number_of_blocks;
    while (nob % 2 == 0) {
      bits_for_index++;
      nob = nob >> 1;
    }

    // If the cache is half the size (split), we will need 1 less bit for the index.
    if (cache_org == sc) bits_for_index --;
  }

  int bits_for_tag = 32 - bits_for_index - bits_for_offset;

  int offset_mask = (1 << bits_for_offset) - 1;
  int index_mask = ((1 << bits_for_index) - 1) << bits_for_offset;
  int tag_mask = ((1 << bits_for_tag) - 1) << (bits_for_index + bits_for_index);

  #ifdef DEBUG
    printf("Block offset mask: %08x\n", offset_mask);
    printf("Index mask: %08x\n", index_mask);
    printf("Tag mask: %08x\n", tag_mask);
  #endif

  int fa_counter1 = 0;
  int fa_counter2 = 0;
  int* fa_counter1_p = &fa_counter1;
  int* fa_counter2_p = &fa_counter2;

  #ifdef DEBUG
    printf("Number of blocks: %d\n", number_of_blocks);
    printf("Bits for offset: %d\n", bits_for_offset);
    printf("Bits for index: %d\n", bits_for_index);
    printf("Bits for tag: %d\n", bits_for_tag);

    printf("Size of cache_block: %d\n", sizeof(cache_block_t));
    printf("Cache size: %d\n", number_of_blocks * sizeof(cache_block_t));
    printf("Half cache size: %d\n", number_of_blocks * sizeof(cache_block_t) / 2);
  #endif

  cache = (cache_block_t*) malloc(number_of_blocks * sizeof(cache_block_t));
  // Pointer arithmetic black magic compiler voodoo will ensure that this is correct
  // (I spent waaaay to long figuring out that '+' doesn't literally mean integer addition when working
  //  with pointers types...)

  cache_block_t* upper_half_cache = cache + (number_of_blocks / 2);

  #ifdef DEBUG
    printf("Cache location: %d\n", cache);
    printf("Upper half cache location: %d\n\n", upper_half_cache);
  #endif
  // exit(1);

  // The global cache memory and global args values should now have been set.

  /* Open the file mem_trace.txt to read memory accesses */
  FILE* ptr_file;
  ptr_file = fopen("mem_trace.txt", "r");
  if (!ptr_file) {
    printf("Unable to open the trace file\n");
    exit(1);
  }

  #ifdef DEBUG
    int line = 0;
  #endif

  /* Loop until whole trace file has been read */
  mem_access_t access;
  while (1) {
    access = read_transaction(ptr_file);
    // If no transactions left, break out of loop
    if (access.address == 0) break;

    /* Do a cache access */
    cache_statistics.accesses++;

    int tag = (access.address & tag_mask) >> bits_for_offset + bits_for_index;
    int index = (access.address & index_mask) >> bits_for_offset;
    bool is_data = access.accesstype == data;

    #ifdef DEBUG
      // printf("Line: %u\n", line++);
      // printf("%c %08x\n", access.accesstype == instruction ? 'I' : 'D', access.address);
      // printf("Tag: %x\n", tag);
      // printf("Index: %x\n", index);
    #endif

    if ( (cache_mapping == fa && cache_org == uc && access_cache_fa(tag, cache, fa_counter1_p, number_of_blocks))
      || (cache_mapping == fa && cache_org == sc && (!is_data
                                                      ? access_cache_fa(tag, cache,            fa_counter1_p, number_of_blocks / 2)
                                                      : access_cache_fa(tag, upper_half_cache, fa_counter2_p, number_of_blocks / 2)))
      || (cache_mapping == dm && cache_org == uc && access_cache_dm(index, tag))
      || (cache_mapping == dm && cache_org == sc && access_cache_dm(index + (number_of_blocks / 2) * is_data, tag))
    ) {
      #ifdef DEBUG
        // printf("Hit!\n");
      #endif
      cache_statistics.hits++;
    }
    #ifdef DEBUG
      // printf("\n");
    #endif
  }

  printf("\nCache Statistics\n");
  printf("-----------------\n\n");
  printf("Accesses: %ld\n", cache_statistics.accesses);
  printf("Hits:     %ld\n", cache_statistics.hits);
  printf("Hit Rate: %.4f\n",
         (double)cache_statistics.hits / cache_statistics.accesses);

  fclose(ptr_file);
  return 0;
}
