/*
 *  GRUB  --  GRand Unified Bootloader
 *  Copyright (C) 2006,2007,2009  Free Software Foundation, Inc.
 *
 *  GRUB is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  GRUB is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with GRUB.  If not, see <http://www.gnu.org/licenses/>.
 */

/* This is an emulation of EFI runtime services.
   This allows a more uniform boot on i386 machines.
   As it emulates only runtime serviceit isn't able
   to chainload EFI bootloader on non-EFI system (TODO) */

#include <grub/symbol.h>
#include <grub/types.h>
#include <grub/efi/api.h>
#include <grub/efiemu/runtime.h>

grub_efi_status_t
efiemu_get_time (grub_efi_time_t *time,
		 grub_efi_time_capabilities_t *capabilities);
grub_efi_status_t
efiemu_set_time (grub_efi_time_t *time);

grub_efi_status_t
efiemu_get_wakeup_time (grub_efi_boolean_t *enabled,
			grub_efi_boolean_t *pending,
			grub_efi_time_t *time);
grub_efi_status_t
efiemu_set_wakeup_time (grub_efi_boolean_t enabled,
			grub_efi_time_t *time);

#ifdef APPLE_CC
#define PHYSICAL_ATTRIBUTE __attribute__ ((section("_text-physical, _text-physical")));
#else
#define PHYSICAL_ATTRIBUTE __attribute__ ((section(".text-physical")));
#endif

grub_efi_status_t
efiemu_set_virtual_address_map (grub_efi_uintn_t memory_map_size,
				grub_efi_uintn_t descriptor_size,
				grub_efi_uint32_t descriptor_version,
				grub_efi_memory_descriptor_t *virtual_map)
  PHYSICAL_ATTRIBUTE;

grub_efi_status_t
efiemu_convert_pointer (grub_efi_uintn_t debug_disposition,
			void **address)
  PHYSICAL_ATTRIBUTE;

grub_efi_status_t
efiemu_get_variable (grub_efi_char16_t *variable_name,
		     grub_efi_guid_t *vendor_guid,
		     grub_efi_uint32_t *attributes,
		     grub_efi_uintn_t *data_size,
		     void *data);

grub_efi_status_t
efiemu_get_next_variable_name (grub_efi_uintn_t *variable_name_size,
			       grub_efi_char16_t *variable_name,
			       grub_efi_guid_t *vendor_guid);

grub_efi_status_t
efiemu_set_variable (grub_efi_char16_t *variable_name,
		     grub_efi_guid_t *vendor_guid,
		     grub_efi_uint32_t attributes,
		     grub_efi_uintn_t data_size,
		     void *data);
grub_efi_status_t
efiemu_get_next_high_monotonic_count (grub_efi_uint32_t *high_count);
void
efiemu_reset_system (grub_efi_reset_type_t reset_type,
		     grub_efi_status_t reset_status,
		     grub_efi_uintn_t data_size,
		     grub_efi_char16_t *reset_data);

grub_efi_status_t
EFI_FUNC (efiemu_set_virtual_address_map) (grub_efi_uintn_t,
					      grub_efi_uintn_t,
					      grub_efi_uint32_t,
					      grub_efi_memory_descriptor_t *)
     PHYSICAL_ATTRIBUTE;
grub_efi_status_t
EFI_FUNC (efiemu_convert_pointer) (grub_efi_uintn_t debug_disposition,
				      void **address)
     PHYSICAL_ATTRIBUTE;
static grub_uint32_t
efiemu_getcrc32 (grub_uint32_t crc, void *buf, int size)
     PHYSICAL_ATTRIBUTE;
static void
init_crc32_table (void)
     PHYSICAL_ATTRIBUTE;
static grub_uint32_t
reflect (grub_uint32_t ref, int len)
     PHYSICAL_ATTRIBUTE;

/*
  The log. It's used when examining memory dump
*/
static grub_uint8_t loge[1000] = "EFIEMULOG";
static int logn = 9;
#define LOG(x)   { if (logn<900) loge[logn++]=x; }

static int ptv_relocated = 0;

/* Interface with grub */
struct grub_efi_runtime_services efiemu_runtime_services;
struct grub_efi_system_table efiemu_system_table;
extern struct grub_efiemu_ptv_rel efiemu_ptv_relloc[];
extern grub_uint8_t efiemu_variables[];
extern grub_uint32_t efiemu_varsize;
extern grub_uint32_t efiemu_high_monotonic_count;
extern grub_int16_t efiemu_time_zone;
extern grub_uint8_t efiemu_time_daylight;
extern grub_uint32_t efiemu_time_accuracy;

/* Some standard functions because we need to be standalone */
static void
efiemu_memcpy (void *to, void *from, int count)
{
  int i;
  for (i = 0; i < count; i++)
    ((grub_uint8_t *) to)[i] = ((grub_uint8_t *) from)[i];
}

static int
efiemu_str16equal (grub_uint16_t *a, grub_uint16_t *b)
{
  grub_uint16_t *ptr1, *ptr2;
  for (ptr1=a,ptr2=b; *ptr1 && *ptr2 == *ptr1; ptr1++, ptr2++);
  return *ptr2 == *ptr1;
}

static grub_size_t
efiemu_str16len (grub_uint16_t *a)
{
  grub_uint16_t *ptr1;
  for (ptr1 = a; *ptr1; ptr1++);
  return ptr1 - a;
}

static int
efiemu_memequal (void *a, void *b, grub_size_t n)
{
  grub_uint8_t *ptr1, *ptr2;
  for (ptr1 = (grub_uint8_t *) a, ptr2 = (grub_uint8_t *)b;
       ptr1 < (grub_uint8_t *)a + n && *ptr2 == *ptr1; ptr1++, ptr2++);
  return ptr1 == a + n;
}

static void
efiemu_memset (grub_uint8_t *a, grub_uint8_t b, grub_size_t n)
{
  grub_uint8_t *ptr1;
  for (ptr1=a; ptr1 < a + n; ptr1++)
    *ptr1 = b;
}

static inline void
write_cmos (grub_uint8_t addr, grub_uint8_t val)
{
  __asm__ __volatile__ ("outb %%al,$0x70\n"
			"mov %%cl, %%al\n"
			"outb %%al,$0x71": :"a" (addr), "c" (val));
}

static inline grub_uint8_t
read_cmos (grub_uint8_t addr)
{
  grub_uint8_t ret;
  __asm__ __volatile__ ("outb %%al, $0x70\n"
			"inb $0x71, %%al": "=a"(ret) :"a" (addr));
  return ret;
}

/* Needed by some gcc versions */
int __stack_chk_fail ()
{
  return 0;
}

/* The function that implement runtime services as specified in
   EFI specification */
static inline grub_uint8_t
bcd_to_hex (grub_uint8_t in)
{
  return 10 * ((in & 0xf0) >> 4) + (in & 0x0f);
}

grub_efi_status_t
EFI_FUNC (efiemu_get_time) (grub_efi_time_t *time,
			       grub_efi_time_capabilities_t *capabilities)
{
  LOG ('a');
  grub_uint8_t state;
  state = read_cmos (0xb);
  if (!(state & (1 << 2)))
    {
      time->year = 2000 + bcd_to_hex (read_cmos (0x9));
      time->month = bcd_to_hex (read_cmos (0x8));
      time->day = bcd_to_hex (read_cmos (0x7));
      time->hour = bcd_to_hex (read_cmos (0x4));
      if (time->hour >= 81)
	time->hour -= 80 - 12;
      if (time->hour == 24)
	time->hour = 0;
      time->minute = bcd_to_hex (read_cmos (0x2));
      time->second = bcd_to_hex (read_cmos (0x0));
    }
  else
    {
      time->year = 2000 + read_cmos (0x9);
      time->month = read_cmos (0x8);
      time->day = read_cmos (0x7);
      time->hour = read_cmos (0x4);
      if (time->hour >= 0x81)
	time->hour -= 0x80 - 12;
      if (time->hour == 24)
	time->hour = 0;
      time->minute = read_cmos (0x2);
      time->second = read_cmos (0x0);
    }
  time->nanosecond = 0;
  time->pad1 = 0;
  time->pad2 = 0;
  time->time_zone = efiemu_time_zone;
  time->daylight = efiemu_time_daylight;
  capabilities->resolution = 1;
  capabilities->accuracy = efiemu_time_accuracy;
  capabilities->sets_to_zero = 0;
  return GRUB_EFI_SUCCESS;
}

grub_efi_status_t
EFI_FUNC (efiemu_set_time) (grub_efi_time_t *time)
{
  LOG ('b');
  grub_uint8_t state;
  state = read_cmos (0xb);
  write_cmos (0xb, state | 0x6);
  write_cmos (0x9, time->year - 2000);
  write_cmos (0x8, time->month);
  write_cmos (0x7, time->day);
  write_cmos (0x4, time->hour);
  write_cmos (0x2, time->minute);
  write_cmos (0x0, time->second);
  efiemu_time_zone = time->time_zone;
  efiemu_time_daylight = time->daylight;
  return GRUB_EFI_SUCCESS;
}

/* Following 2 functions are vendor specific. So announce it as unsupported */
grub_efi_status_t
EFI_FUNC (efiemu_get_wakeup_time) (grub_efi_boolean_t *enabled,
				      grub_efi_boolean_t *pending,
				      grub_efi_time_t *time)
{
  LOG ('c');
  return GRUB_EFI_UNSUPPORTED;
}

grub_efi_status_t
EFI_FUNC (efiemu_set_wakeup_time) (grub_efi_boolean_t enabled,
				      grub_efi_time_t *time)
{
  LOG ('d');
  return GRUB_EFI_UNSUPPORTED;
}

static grub_uint32_t crc32_table [256];

static grub_uint32_t
reflect (grub_uint32_t ref, int len)
{
  grub_uint32_t result = 0;
  int i;

  for (i = 1; i <= len; i++)
    {
      if (ref & 1)
	result |= 1 << (len - i);
      ref >>= 1;
    }

  return result;
}

static void
init_crc32_table (void)
{
  grub_uint32_t polynomial = 0x04c11db7;
  int i, j;

  for(i = 0; i < 256; i++)
    {
      crc32_table[i] = reflect(i, 8) << 24;
      for (j = 0; j < 8; j++)
        crc32_table[i] = (crc32_table[i] << 1) ^
            (crc32_table[i] & (1 << 31) ? polynomial : 0);
      crc32_table[i] = reflect(crc32_table[i], 32);
    }
}

static grub_uint32_t
efiemu_getcrc32 (grub_uint32_t crc, void *buf, int size)
{
  int i;
  grub_uint8_t *data = buf;

  if (! crc32_table[1])
    init_crc32_table ();

  crc^= 0xffffffff;

  for (i = 0; i < size; i++)
    {
      crc = (crc >> 8) ^ crc32_table[(crc & 0xFF) ^ *data];
      data++;
    }

  return crc ^ 0xffffffff;
}


grub_efi_status_t EFI_FUNC
(efiemu_set_virtual_address_map) (grub_efi_uintn_t memory_map_size,
				  grub_efi_uintn_t descriptor_size,
				  grub_efi_uint32_t descriptor_version,
				  grub_efi_memory_descriptor_t *virtual_map)
{
  struct grub_efiemu_ptv_rel *cur_relloc;

  LOG ('e');

  /* Ensure that we are called only once */
  if (ptv_relocated)
    return GRUB_EFI_UNSUPPORTED;
  ptv_relocated = 1;

  /* Correct addresses using information supplied by grub */
  for (cur_relloc = efiemu_ptv_relloc; cur_relloc->size;cur_relloc++)
    {
      grub_int64_t corr = 0;
      grub_efi_memory_descriptor_t *descptr;

      /* Compute correction */
      for (descptr = virtual_map;
	   ((grub_uint8_t *) descptr - (grub_uint8_t *) virtual_map)
	     < memory_map_size;
	   descptr = (grub_efi_memory_descriptor_t *)
	     ((grub_uint8_t *) descptr + descriptor_size))
	{
	  if (descptr->type == cur_relloc->plustype)
	    corr += descptr->virtual_start - descptr->physical_start;
	  if (descptr->type == cur_relloc->minustype)
	    corr -= descptr->virtual_start - descptr->physical_start;
	}

      /* Apply correction */
      switch (cur_relloc->size)
	{
	case 8:
	  *((grub_uint64_t *) UINT_TO_PTR (cur_relloc->addr)) += corr;
	  break;
	case 4:
	  *((grub_uint32_t *) UINT_TO_PTR (cur_relloc->addr)) += corr;
	  break;
	case 2:
	  *((grub_uint16_t *) UINT_TO_PTR (cur_relloc->addr)) += corr;
	  break;
	case 1:
	  *((grub_uint8_t *) UINT_TO_PTR (cur_relloc->addr)) += corr;
	  break;
	}
    }

  /* Recompute crc32 of system table and runtime services */
  efiemu_system_table.hdr.crc32 = 0;
  efiemu_system_table.hdr.crc32 = efiemu_getcrc32
    (0, &efiemu_system_table, sizeof (efiemu_system_table));

  efiemu_runtime_services.hdr.crc32 = 0;
  efiemu_runtime_services.hdr.crc32 = efiemu_getcrc32
    (0, &efiemu_runtime_services, sizeof (efiemu_runtime_services));

  return GRUB_EFI_SUCCESS;
}

/* since efiemu_set_virtual_address_map corrects all the pointers
   we don't need efiemu_convert_pointer */
grub_efi_status_t
EFI_FUNC (efiemu_convert_pointer) (grub_efi_uintn_t debug_disposition,
				      void **address)
{
  LOG ('f');
  return GRUB_EFI_UNSUPPORTED;
}

/* Next comes variable services. Because we have no vendor-independent
   way to store these variables we have no non-volatility */

/* Find variable by name and GUID. */
static struct efi_variable *
find_variable (grub_efi_guid_t *vendor_guid,
	       grub_efi_char16_t *variable_name)
{
  grub_uint8_t *ptr;
  struct efi_variable *efivar;

  for (ptr = efiemu_variables; ptr < efiemu_variables + efiemu_varsize; )
    {
      efivar = (struct efi_variable *) ptr;
      if (!efivar->namelen)
	return 0;
      if (efiemu_str16equal((grub_efi_char16_t *)(efivar + 1), variable_name)
	  && efiemu_memequal (&(efivar->guid), vendor_guid,
			      sizeof (efivar->guid)))
	return efivar;
      ptr += efivar->namelen + efivar->size + sizeof (*efivar);
    }
  return 0;
}

grub_efi_status_t
EFI_FUNC (efiemu_get_variable) (grub_efi_char16_t *variable_name,
				   grub_efi_guid_t *vendor_guid,
				   grub_efi_uint32_t *attributes,
				   grub_efi_uintn_t *data_size,
				   void *data)
{
  struct efi_variable *efivar;
  LOG ('g');
  efivar = find_variable (vendor_guid, variable_name);
  if (!efivar)
    return GRUB_EFI_NOT_FOUND;
  if (*data_size < efivar->size)
    {
      *data_size = efivar->size;
      return GRUB_EFI_BUFFER_TOO_SMALL;
    }
  *data_size = efivar->size;
  efiemu_memcpy (data, (grub_uint8_t *)(efivar + 1) + efivar->namelen,
		 efivar->size);
  *attributes = efivar->attributes;

  return GRUB_EFI_SUCCESS;
}

grub_efi_status_t EFI_FUNC
(efiemu_get_next_variable_name) (grub_efi_uintn_t *variable_name_size,
				 grub_efi_char16_t *variable_name,
				 grub_efi_guid_t *vendor_guid)
{
  struct efi_variable *efivar;
  LOG ('l');

  if (!variable_name_size || !variable_name || !vendor_guid)
    return GRUB_EFI_INVALID_PARAMETER;
  if (variable_name[0])
    {
      efivar = find_variable (vendor_guid, variable_name);
      if (!efivar)
	return GRUB_EFI_NOT_FOUND;
      efivar = (struct efi_variable *)((grub_uint8_t *)efivar
				       + efivar->namelen
				       + efivar->size + sizeof (*efivar));
    }
  else
    efivar = (struct efi_variable *) (efiemu_variables);

  LOG ('m');
  if ((grub_uint8_t *)efivar >= efiemu_variables + efiemu_varsize
      || !efivar->namelen)
    return GRUB_EFI_NOT_FOUND;
  if (*variable_name_size < efivar->namelen)
    {
      *variable_name_size = efivar->namelen;
      return GRUB_EFI_BUFFER_TOO_SMALL;
    }

  efiemu_memcpy (variable_name, efivar + 1, efivar->namelen);
  efiemu_memcpy (vendor_guid, &(efivar->guid),
		 sizeof (efivar->guid));

  LOG('h');
  return GRUB_EFI_SUCCESS;
}

grub_efi_status_t
EFI_FUNC (efiemu_set_variable) (grub_efi_char16_t *variable_name,
				   grub_efi_guid_t *vendor_guid,
				   grub_efi_uint32_t attributes,
				   grub_efi_uintn_t data_size,
				   void *data)
{
  struct efi_variable *efivar;
  grub_uint8_t *ptr;
  LOG('i');
  if (!variable_name[0])
    return GRUB_EFI_INVALID_PARAMETER;
  efivar = find_variable (vendor_guid, variable_name);

  /* Delete variable if any */
  if (efivar)
    {
      efiemu_memcpy (efivar, (grub_uint8_t *)(efivar + 1)
		     + efivar->namelen + efivar->size,
		     (efiemu_variables + efiemu_varsize)
		     - ((grub_uint8_t *)(efivar + 1)
			+ efivar->namelen + efivar->size));
      efiemu_memset (efiemu_variables + efiemu_varsize
		     - (sizeof (*efivar) + efivar->namelen + efivar->size),
		     0, (sizeof (*efivar) + efivar->namelen + efivar->size));
    }

  if (!data_size)
    return GRUB_EFI_SUCCESS;

  for (ptr = efiemu_variables; ptr < efiemu_variables + efiemu_varsize; )
    {
      efivar = (struct efi_variable *) ptr;
      ptr += efivar->namelen + efivar->size + sizeof (*efivar);
      if (!efivar->namelen)
	break;
    }
  if ((grub_uint8_t *)(efivar + 1) + data_size
      + 2 * (efiemu_str16len (variable_name) + 1)
      >= efiemu_variables + efiemu_varsize)
    return GRUB_EFI_OUT_OF_RESOURCES;

  efiemu_memcpy (&(efivar->guid), vendor_guid, sizeof (efivar->guid));
  efivar->namelen = 2 * (efiemu_str16len (variable_name) + 1);
  efivar->size = data_size;
  efivar->attributes = attributes;
  efiemu_memcpy (efivar + 1, variable_name,
		 2 * (efiemu_str16len (variable_name) + 1));
  efiemu_memcpy ((grub_uint8_t *)(efivar + 1)
		 + 2 * (efiemu_str16len (variable_name) + 1),
		 data, data_size);

  return GRUB_EFI_SUCCESS;
}

grub_efi_status_t EFI_FUNC
(efiemu_get_next_high_monotonic_count) (grub_efi_uint32_t *high_count)
{
  LOG ('j');
  if (!high_count)
    return GRUB_EFI_INVALID_PARAMETER;
  *high_count = ++efiemu_high_monotonic_count;
  return GRUB_EFI_SUCCESS;
}

/* To implement it with APM we need to go to real mode. It's too much hassle
   Besides EFI specification says that this function shouldn't be used
   on systems supporting ACPI
 */
void
EFI_FUNC (efiemu_reset_system) (grub_efi_reset_type_t reset_type,
				   grub_efi_status_t reset_status,
				   grub_efi_uintn_t data_size,
				   grub_efi_char16_t *reset_data)
{
  LOG ('k');
}

struct grub_efi_runtime_services efiemu_runtime_services =
{
  .hdr =
  {
    .signature = GRUB_EFIEMU_RUNTIME_SERVICES_SIGNATURE,
    .revision = 0x0001000a,
    .header_size = sizeof (struct grub_efi_runtime_services),
    .crc32 = 0, /* filled later*/
    .reserved = 0
  },
  .get_time = efiemu_get_time,
  .set_time = efiemu_set_time,
  .get_wakeup_time = efiemu_get_wakeup_time,
  .set_wakeup_time = efiemu_set_wakeup_time,

  .set_virtual_address_map = efiemu_set_virtual_address_map,
  .convert_pointer = efiemu_convert_pointer,

  .get_variable = efiemu_get_variable,
  .get_next_variable_name = efiemu_get_next_variable_name,
  .set_variable = efiemu_set_variable,
  .get_next_high_monotonic_count = efiemu_get_next_high_monotonic_count,

  .reset_system = efiemu_reset_system
};


static grub_uint16_t efiemu_vendor[] =
  {'G', 'R', 'U', 'B', ' ', 'E', 'F', 'I', ' ',
   'R', 'U', 'N', 'T', 'I', 'M', 'E', 0};

struct grub_efi_system_table efiemu_system_table =
{
  .hdr =
  {
    .signature = GRUB_EFIEMU_SYSTEM_TABLE_SIGNATURE,
    .revision = 0x0001000a,
    .header_size = sizeof (struct grub_efi_system_table),
    .crc32 = 0, /* filled later*/
    .reserved = 0
  },
  .firmware_vendor = efiemu_vendor,
  .firmware_revision = 0x0001000a,
  .console_in_handler = 0,
  .con_in = 0,
  .console_out_handler = 0,
  .con_out = 0,
  .standard_error_handle = 0,
  .std_err = 0,
  .runtime_services = &efiemu_runtime_services,
  .boot_services = 0,
  .num_table_entries = 0,
  .configuration_table = 0
};