/* cpu2phys.c */
/*****************************************************************************/
/* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only                     */
/*                                                                           */
/* AS                                                                        */
/*                                                                           */
/* CPU->Physical Address Translation                                         */
/*                                                                           */
/*****************************************************************************/

#include "stdinc.h"
#include <stddef.h>
#include <assert.h>
#include <string.h>
#include "strutil.h"
#include "tempresult.h"
#include "asmdef.h"
#include "cpu2phys.h"

typedef struct
{
  LargeWord cpu_start, cpu_end;
  LargeWord phys_start, phys_end;
} cpu_2_phys_area_t;

typedef struct
{
  cpu_2_phys_area_t *areas;
  size_t capacity;
  size_t cnt;
  LargeWord end;
} cpu_2_phys_area_list_t;

static cpu_2_phys_area_list_t area_lists[SegCount];
static void (*current_check)(void);

/*!------------------------------------------------------------------------
 * \fn     cpu_2_phys_area_t *insert_area(cpu_2_phys_area_list_t *p_list, unsigned index)
 * \brief  make space for area at given index, implicitly increasing array size
 * \param  p_list list to operate on
 * \param  index place to insert
 * \return * to freed and cleared area
 * ------------------------------------------------------------------------ */

static cpu_2_phys_area_t *insert_area(cpu_2_phys_area_list_t *p_list, unsigned index)
{
  cpu_2_phys_area_t *p_ret;

  assert(index <= p_list->cnt);

  if (p_list->cnt >= p_list->capacity)
  {
    size_t new_capacity = p_list->cnt + 1;
    size_t new_alloc = new_capacity * sizeof(*p_list->areas);
    p_list->areas = p_list->capacity
                  ? (cpu_2_phys_area_t*) realloc(p_list->areas, new_alloc)
                  : (cpu_2_phys_area_t*) malloc(new_alloc);
    p_list->capacity = new_capacity;
  }
  p_ret = &p_list->areas[index];
  p_list->cnt++;
  memmove(p_ret + 1, p_ret, sizeof(*p_ret) * (p_list->cnt - 1 - index));
  memset(p_ret, 0, sizeof(*p_ret));
  return p_ret;
}

/*!------------------------------------------------------------------------
 * \fn     delete_area(cpu_2_phys_area_list_t *p_list, unsigned index)
 * \brief  delete entry from table
 * \param  p_list list to operate on
 * \param  index place to delete
 * ------------------------------------------------------------------------ */

static void delete_area(cpu_2_phys_area_list_t *p_list, unsigned index)
{
  cpu_2_phys_area_t *p_ret;

  assert(index < p_list->cnt);
  p_ret = &p_list->areas[index];
  memmove(p_ret, p_ret + 1, sizeof(*p_ret) * (p_list->cnt - 1 - index));
  p_list->cnt--;
}

/*!------------------------------------------------------------------------
 * \fn     get_list(as_addrspace_t addr_space)
 * \brief  retrieve list per address space
 * \param  addr_space address space
 * \return * to list
 * ------------------------------------------------------------------------ */

static cpu_2_phys_area_list_t *get_list(as_addrspace_t addr_space)
{
  assert(addr_space < SegCount);
  return &area_lists[addr_space];
}

/*!------------------------------------------------------------------------
 * \fn     cpu_2_phys_set_current_check(void (*fnc)(void))
 * \brief  set function to check for up-to-date-ness of transalation tables
 * \param  fnc new function to set
 * ------------------------------------------------------------------------ */

void cpu_2_phys_set_current_check(void (*fnc)(void))
{
  current_check = fnc;
}

/*!------------------------------------------------------------------------
 * \fn     cpu_2_phys_area_clear(as_addrspace_t addr_space)
 * \brief  clear tables before adding new ones
 * \param  addr_space address space to operate on
 * ------------------------------------------------------------------------ */

void cpu_2_phys_area_clear(as_addrspace_t addr_space)
{
  assert(addr_space < SegCount);
  area_lists[addr_space].cnt = 0;
  area_lists[addr_space].end = 0;
}

/*!------------------------------------------------------------------------
 * \fn     cpu_2_phys_area_add(as_addrspace_t addr_space, LargeWord new_cpu_start, LargeWord new_phys_start, LargeWord new_len)
 * \brief  add another area
 * \param  addr_space address space to operate on
 * \param  new_cpu_start start address in CPU address space
 * \param  new_phys_start start address in physical address space
 * \param  new_len area size in <gran>
 * ------------------------------------------------------------------------ */

void cpu_2_phys_area_add(as_addrspace_t addr_space, LargeWord new_cpu_start, LargeWord new_phys_start, LargeWord new_len)
{
  size_t z, z2;
  cpu_2_phys_area_list_t *p_list = get_list(addr_space);
  cpu_2_phys_area_t *p_area;
  LargeWord new_cpu_end = new_cpu_start + (new_len - 1);
  LargeWord overlap_start, overlap_end, delta;
  Boolean change;

  /* Do not add zero-length areas */

  if (!new_len)
    return;

  /* sort in according to CPU start address */

  for (z = 0; z < p_list->cnt; z++)
    if (p_list->areas[z].cpu_start >= new_cpu_start)
      break;

  p_area = insert_area(p_list, z);
  p_area->cpu_start = new_cpu_start;
  p_area->cpu_end = new_cpu_end;
  p_area->phys_start = new_phys_start;
  p_area->phys_end = new_phys_start + (new_len - 1);

  /* shrink/delete (partially) overlapping areas: */

  do
  {
    change = False;
    for (z2 = 0; z2 < p_list->cnt; z2++)
    {
      /* do not test overlap with new entry itself */

      if (z2 == z)
        continue;

      /* deduce overlapping area */

      overlap_start = max(p_list->areas[z].cpu_start, p_list->areas[z2].cpu_start);
      overlap_end = min(p_list->areas[z].cpu_end, p_list->areas[z2].cpu_end);
      if (overlap_start >= overlap_end)
        continue;

      /* Delete old area entirely? -> possibly correct index of new entry and restart */

      if ((overlap_start == p_list->areas[z2].cpu_start)
       && (overlap_end == p_list->areas[z2].cpu_end))
      {
        delete_area(p_list, z2);
        if (z2 < z)
          z--;
        change = True;
        break;
      }

      /* shorten old area at beginning? */

      else if (overlap_start == p_list->areas[z2].cpu_start)
      {
        delta = overlap_end - overlap_start + 1;
        p_list->areas[z2].cpu_start += delta;
        p_list->areas[z2].phys_start += delta;
      }

      /* shorten old area at end? */

      else if (overlap_end == p_list->areas[z2].cpu_end)
      {
        delta = overlap_end - overlap_start + 1;
        p_list->areas[z2].cpu_end -= delta;
        p_list->areas[z2].phys_end -= delta;
      }

      /* Overlap cuts out in the mid, split into parts. Assuming the addresses were sorted,
         z2 equals z - 1 and the old area's part surround the new one: */

      else
      {
        cpu_2_phys_area_t save = p_list->areas[z2];

        delta = save.cpu_end - overlap_start + 1;
        p_list->areas[z2].cpu_end -= delta;
        p_list->areas[z2].phys_end -= delta;
        p_area = insert_area(p_list, z + 1);
        *p_area = save;
        delta = overlap_end - save.cpu_start + 1;
        p_area->cpu_start += delta;
        p_area->phys_start += delta;
        change = True;
        break;
      }
    }
  }
  while (change);
}

/*!------------------------------------------------------------------------
 * \fn     cpu_2_phys_area_set_cpu_end(as_addrspace_t addr_space, LargeWord cpu_end)
 * \brief  set the end of the CPU's address space
 * \param  addr_space address space to operate on
 * \param  cpu_end end of CPU address space
 * ------------------------------------------------------------------------ */

void cpu_2_phys_area_set_cpu_end(as_addrspace_t addr_space, LargeWord cpu_end)
{
  cpu_2_phys_area_list_t *p_list = get_list(addr_space);

  p_list->end = cpu_end;
}

/*!------------------------------------------------------------------------
 * \fn     cpu_2_phys_area_fill(as_addrspace_t addr_space, LargeWord cpu_start, LargeWord cpu_end)
 * \brief  fill gaps in the CPU-side address space with 1:1 mappings
 * \param  addr_space address space to operate on
 * \param  cpu_start start of CPU address space
 * \param  cpu_end end of CPU address space
 * ------------------------------------------------------------------------ */

void cpu_2_phys_area_fill(as_addrspace_t addr_space, LargeWord cpu_start, LargeWord cpu_end)
{
  LargeWord expected_start;
  size_t z;
  cpu_2_phys_area_list_t *p_list = get_list(addr_space);
  cpu_2_phys_area_t *p_area;

  /* For each mapping, check whether there is no gap between its start address
     and the predecessor's end address.  If so, insert a 1:1 mapped area to
     fill the gap: */

  z = 0;
  while (z < p_list->cnt)
  {
    expected_start = z ? p_list->areas[z - 1].cpu_end + 1 : cpu_start;

    /* no gap -> just continue with the next entry */

    if ((z < p_list->cnt) && (p_list->areas[z].cpu_start <= expected_start))
    {
      z++;
      continue;
    }
    p_area = insert_area(p_list, z);
    p_area->cpu_start = 
    p_area->phys_start = expected_start;
    p_area->cpu_end =
    p_area->phys_end = (z + 1 < p_list->cnt) ? p_list->areas[z + 1].cpu_start - 1 : cpu_end;

    /* We know the entries at z (freshly inserted) and z+1 (moved up one index) are
       continuous, so we may increase the counter by two: */

    z += 2;
  }

  /* Do the same test once again at the very end of the array, to be sure it covers everything
     up to the given cpu_end: */

  if (!p_list->cnt || p_list->areas[p_list->cnt - 1].cpu_end < cpu_end)
  {
    expected_start = p_list->cnt ? p_list->areas[p_list->cnt - 1].cpu_end + 1 : cpu_start;
    p_area = insert_area(p_list, p_list->cnt);
    p_area->cpu_start = 
    p_area->phys_start = expected_start;
    p_area->cpu_end =
    p_area->phys_end = cpu_end;
  }

  /* Save the cpu_end for usage in phys_2_cpu(): */

  cpu_2_phys_area_set_cpu_end(addr_space, cpu_end);
}

/*!------------------------------------------------------------------------
 * \fn     cpu_2_phys_area_dump(as_addrspace_t addr_space, FILE *p_file)
 * \brief  output current mapping
 * \param  addr_space address space to operate on
 * \param  p_file where to dump
 * ------------------------------------------------------------------------ */

void cpu_2_phys_area_dump(as_addrspace_t addr_space, FILE *p_file)
{
  cpu_2_phys_area_list_t *p_list = get_list(addr_space);
  char str[100];
  size_t z;
  int cpu_max_len = 0, phys_max_len = 0;

  for (z = 0; z < p_list->cnt; z++)
  {
    int this_len;

    this_len = as_snprintf(str, sizeof str, "%lllx", p_list->areas[z].cpu_end);
    if (this_len > cpu_max_len)
      cpu_max_len = this_len;
    this_len = as_snprintf(str, sizeof str, "%lllx", p_list->areas[z].phys_end);
    if (this_len > phys_max_len)
      phys_max_len = this_len;
  }
  for (z = 0; z < p_list->cnt; z++)
  {
    as_snprintf(str, sizeof str, "[%2u] %0*lllx...%0*lllx -> %0*lllx...%0*lllx\n", z,
                cpu_max_len, p_list->areas[z].cpu_start,
                cpu_max_len, p_list->areas[z].cpu_end,
                phys_max_len, p_list->areas[z].phys_start,
                phys_max_len, p_list->areas[z].phys_end);
    fputs(str, p_file);
  }
}

/*!------------------------------------------------------------------------
 * \fn     def_phys_2_cpu(as_addrspace_t addr_space, LargeWord *p_address)
 * \brief  physical -> CPU address translation
 * \param  addr_space address space to operate on
 * \param  p_address address to translate
 * \return True if translation succeeded
 * ------------------------------------------------------------------------ */

Boolean def_phys_2_cpu(as_addrspace_t addr_space, LargeWord *p_address)
{
  cpu_2_phys_area_list_t *p_list;
  size_t win;

  if (current_check) current_check();
  p_list = get_list(addr_space);
  for (win = 0; win < p_list->cnt; win++)
    if ((*p_address >= p_list->areas[win].phys_start) && (*p_address <= p_list->areas[win].phys_end))
    {
      *p_address = (*p_address - p_list->areas[win].phys_start) + p_list->areas[win].cpu_start;
      return True;
    }
  return False;
}

/*!------------------------------------------------------------------------
 * \fn     def_cpu_2_phys(as_addrspace_t addr_space, LargeWord *p_address)
 * \brief  CPU -> physical address translation
 * \param  addr_space address space to operate on
 * \param  p_address address to translate
 * \return True if translation succeeded
 * ------------------------------------------------------------------------ */

Boolean def_cpu_2_phys(as_addrspace_t addr_space, LargeWord *p_address)
{
  cpu_2_phys_area_list_t *p_list;
  size_t win;

  if (current_check) current_check();
  p_list = get_list(addr_space);
  for (win = 0; win < p_list->cnt; win++)
    if ((*p_address >= p_list->areas[win].cpu_start) && (*p_address <= p_list->areas[win].cpu_end))
    {
      *p_address = (*p_address - p_list->areas[win].cpu_start) + p_list->areas[win].phys_start;
      return True;
    }
  return False;
}

/*!------------------------------------------------------------------------
 * \fn     fnc_phys_2_cpu(TempResult *p_ret, const TempResult *p_args, unsigned arg_cnt)
 * \brief  built-in function for physical -> CPU address translation
 * \param  p_ret returns CPU address
 * \param  p_args physical address argument
 * \return True if translation possible at all
 * ------------------------------------------------------------------------ */

Boolean fnc_phys_2_cpu(TempResult *p_ret, const TempResult *p_args, unsigned arg_cnt)
{
  LargeWord address;

  UNUSED(arg_cnt);

  if (!area_lists[ActPC].cnt)
    return False;

  address = p_args[0].Contents.Int;
  as_tempres_set_int(p_ret, def_phys_2_cpu(ActPC, &address) ? address : area_lists[ActPC].end + 1);
  return True;
}

/*!------------------------------------------------------------------------
 * \fn     fnc_cpu_2_phys(TempResult *p_ret, const TempResult *p_args, unsigned arg_cnt)
 * \brief  built-in function for CPU -> physical address translation
 * \param  p_ret returns physical address
 * \param  p_args CPU address argument
 * \return True if translation possible at all
 * ------------------------------------------------------------------------ */

Boolean fnc_cpu_2_phys(TempResult *p_ret, const TempResult *p_args, unsigned arg_cnt)
{
  LargeWord address;

  UNUSED(arg_cnt);

  if (!area_lists[ActPC].cnt)
    return False;

  address = p_args[0].Contents.Int;
  as_tempres_set_int(p_ret, def_cpu_2_phys(ActPC, &address) ? address : SegLimits[ActPC] + 1);
  return True;
}
