// SPDX-License-Identifier: MPL-2.0
#include "devs/apic/lapic.h"
#include "memory/memory.h"
#include "irq/irq.h"
#include "cpu/tsc.h"
typedef struct {
  sh_lapic_DEVICE **by_apic_id;
  sh_lapic_DEVICE **by_cpu_id;
  sh_uint64 max_apic_id;
  sh_uint64 max_acpi_processor_id;
  sh_uint64 lapic_count;
} lapic_devs_template;
lapic_devs_template lapic_devs={0};
sh_uint64 lapic_frequency=0;
static inline sh_uint32 sh_lapic_read(sh_lapic_DEVICE *lapic,sh_uint32 reg) {
  return *(volatile sh_uint32*)((sh_uint8*)lapic->base+reg);
}
static inline void sh_lapic_write(sh_lapic_DEVICE *lapic,sh_uint32 reg,sh_uint32 value) {
  *(volatile sh_uint32*)((sh_uint8*)lapic->base+reg)=value;
}
SH_STATUS sh_lapic_init(sh_uint64 lapic_phys,sh_uint8 spurious_vector,sh_uint8 apic_id,sh_uint8 apic_processor_id,sh_uint32 flags,sh_lapic_DEVICE *lapic) {
  if (lapic==SH_NULLPTR || lapic_phys==0) return SH_STATUS_INVALID_PARAMETER;
  sh_uint64 page=lapic_phys& ~0xfffull;
  SH_STATUS status=sh_memory_identity_map(page,1,SH_PAGE_RW | SH_PAGE_PRESENT | SH_PAGE_NX | SH_PAGE_PCD);
  if (sh_status_error(status) && status!=SH_STATUS_ERROR_VA_FULLY_MAPPED) return status;
  lapic->base=(volatile sh_uint32*)lapic_phys;
  lapic->spurious_vector=spurious_vector;
  lapic->apic_id=apic_id;
  lapic->acpi_processor_id=apic_processor_id;
  lapic->flags=flags;
  return SH_STATUS_SUCCESS;
}
SH_STATUS sh_lapic_init_dev(sh_uint8 spurious_vector,sh_lapic_DEVICE *lapic) {
  if (lapic==SH_NULLPTR) return SH_STATUS_INVALID_PARAMETER;
  sh_uint32 svr=spurious_vector|(1<<8);
  sh_lapic_write(lapic,SH_LAPIC_OFFSET_SVR,svr);
  sh_lapic_write(lapic,SH_LAPIC_OFFSET_ESR,0);
  sh_lapic_write(lapic,SH_LAPIC_OFFSET_ESR,0);
  sh_lapic_write(lapic,SH_LAPIC_OFFSET_EOI,0);
  return SH_STATUS_SUCCESS;
}
SH_STATUS sh_lapic_init_devs(sh_uint16 max_lapic_id,sh_uint16 max_acpi_processor_id) {
  if (max_lapic_id==0 || max_lapic_id>256) return SH_STATUS_INVALID_PARAMETER;
  sh_lapic_DEVICE **lapic_ptr_by_apic_id=sh_malloc((max_lapic_id+1)*sizeof(sh_lapic_DEVICE*));
  if (!lapic_ptr_by_apic_id) return SH_STATUS_ERROR_NULLPTR_RETURNED;
  sh_mem_set_8((sh_uint8*)lapic_ptr_by_apic_id,0x0,(max_lapic_id+1)*sizeof(sh_lapic_DEVICE*));
  lapic_devs.by_apic_id=lapic_ptr_by_apic_id;
  sh_lapic_DEVICE **lapic_ptr_by_cpu_id=sh_malloc((max_acpi_processor_id+1)*sizeof(sh_lapic_DEVICE*));
  if (!lapic_ptr_by_cpu_id) return SH_STATUS_ERROR_NULLPTR_RETURNED;
  sh_mem_set_8((sh_uint8*)lapic_ptr_by_cpu_id,0x0,(max_acpi_processor_id+1)*sizeof(sh_lapic_DEVICE*));
  lapic_devs.by_cpu_id=lapic_ptr_by_cpu_id;
  lapic_devs.max_apic_id=max_lapic_id;
  lapic_devs.max_acpi_processor_id=max_acpi_processor_id;
  return SH_STATUS_SUCCESS;
}
SH_STATUS sh_lapic_bind(sh_lapic_DEVICE *lapic_dev) {
  if (lapic_dev==SH_NULLPTR) return SH_STATUS_INVALID_PARAMETER;
  if (lapic_devs.by_apic_id==SH_NULLPTR || lapic_devs.by_cpu_id==SH_NULLPTR) return SH_STATUS_DEVS_NOT_INITIALIZED;
  lapic_devs.by_apic_id[lapic_dev->apic_id]=lapic_dev;
  lapic_devs.by_cpu_id[lapic_dev->acpi_processor_id]=lapic_dev;
  lapic_devs.lapic_count++;
  return SH_STATUS_SUCCESS;
}
void sh_lapic_eoi(sh_lapic_DEVICE *lapic) {
  sh_lapic_write(lapic,SH_LAPIC_OFFSET_EOI,0);
}
sh_lapic_DEVICE *sh_lapic_get_dev_apic_id(sh_uint64 apic_id) {
  if (lapic_devs.by_apic_id==SH_NULLPTR) return SH_NULLPTR;
  if (apic_id>lapic_devs.max_apic_id) return SH_NULLPTR;
  return lapic_devs.by_apic_id[apic_id];
}
sh_lapic_DEVICE *sh_lapic_get_dev_acpi_cpu_id(sh_uint64 acpi_processor_id) {
  if (lapic_devs.by_cpu_id==SH_NULLPTR) return SH_NULLPTR;
  if (acpi_processor_id>lapic_devs.max_apic_id) return SH_NULLPTR;
  return lapic_devs.by_apic_id[acpi_processor_id];
}
SH_STATUS sh_lapic_timer_one_shot(sh_lapic_DEVICE *lapic_dev,sh_uint32 initial_value) {
  if (lapic_dev==SH_NULLPTR || initial_value==0) return SH_STATUS_INVALID_PARAMETER;
  sh_lapic_write(lapic_dev,SH_LAPIC_OFFSET_LVT_TIMER,(1<<16));
  sh_lapic_write(lapic_dev,SH_LAPIC_OFFSET_TIMER_DIV,SH_LAPIC_TIMER_DIVIDER);
  sh_lapic_write(lapic_dev,SH_LAPIC_OFFSET_TIMER_INIT,initial_value);
  sh_uint32 lvt=(0xFE)|(0<<17)|(0<<16);
  sh_lapic_write(lapic_dev,SH_LAPIC_OFFSET_LVT_TIMER,lvt);
  return SH_STATUS_SUCCESS;
}
SH_STATUS sh_lapic_calibrate(sh_lapic_DEVICE *lapic_dev,sh_uint64 cpu_freq) {
  if (!lapic_dev) return SH_STATUS_INVALID_PARAMETER;
  sh_lapic_write(lapic_dev,SH_LAPIC_OFFSET_TIMER_DIV,SH_LAPIC_TIMER_DIVIDER);
  sh_lapic_write(lapic_dev,SH_LAPIC_OFFSET_LVT_TIMER,(1<<16));
  sh_lapic_write(lapic_dev,SH_LAPIC_OFFSET_TIMER_INIT,0xFFFFFFFF);
  sh_uint64 tsc_start=sh_tsc_read_tsc();
  sh_uint32 lapic_start=sh_lapic_read(lapic_dev,SH_LAPIC_OFFSET_TIMER_CURR);
  for (volatile int i=0;i<10000000;i++) __asm__ volatile("pause");
  sh_uint32 lapic_end=sh_lapic_read(lapic_dev,SH_LAPIC_OFFSET_TIMER_CURR);
  sh_uint64 tsc_end=sh_tsc_read_tsc();
  sh_uint32 lapic_delta=lapic_start-lapic_end;
  sh_uint64 tsc_delta=tsc_end-tsc_start;
  if (!tsc_delta || !lapic_delta) return SH_STATUS_INVALID_PARAMETER;
  sh_uint64 lapic_freq=(lapic_delta*cpu_freq)/tsc_delta;
  lapic_frequency=lapic_freq;
  return SH_STATUS_SUCCESS;
}
sh_uint64 sh_lapic_get_frequency() {
  return lapic_frequency;
}
SH_STATUS sh_lapic_timer_one_shot_us(sh_lapic_DEVICE *lapic_dev,sh_uint64 microseconds_count) {
  sh_asm_sti();
  if (lapic_frequency==0) return SH_STATUS_INVALID_STATE;
  if (microseconds_count==0) return SH_STATUS_SUCCESS;
  sh_uint64 ticks=(lapic_frequency*microseconds_count)/1000000ULL;
  if (ticks>0xFFFFFFFF) return SH_STATUS_TOO_MUCH_DATA;
  if (ticks==0) ticks=1;
  sh_lapic_write(lapic_dev,SH_LAPIC_OFFSET_LVT_TIMER,(1<<16));
  sh_lapic_write(lapic_dev,SH_LAPIC_OFFSET_LVT_TIMER,0xFE);
  sh_lapic_write(lapic_dev,SH_LAPIC_OFFSET_TIMER_INIT,(sh_uint32)ticks);
  sh_irq_start_timer();
  while (sh_irq_get_timer_state()) {
    __asm__ volatile ("hlt");
  }
  return SH_STATUS_SUCCESS;
}
sh_lapic_DEVICE **sh_lapic_get_by_apic_id_array() {
  return lapic_devs.by_apic_id;
}
sh_uint64 sh_lapic_get_max_apic_id() {
  return lapic_devs.max_apic_id;
}
sh_uint64 sh_lapic_get_max_acpi_processor_id() {
  return lapic_devs.max_acpi_processor_id;
}
sh_uint64 sh_lapic_get_lapic_count() {
  return lapic_devs.lapic_count;
}
sh_bool sh_lapic_ipi_is_busy(sh_lapic_DEVICE *lapic_dev) {
  if (!lapic_dev) return SH_FALSE;
  return (sh_lapic_read(lapic_dev,SH_LAPIC_OFFSET_ICR_LOW)&(1u<<12))?SH_TRUE:SH_FALSE;
}
sh_int16 sh_lapic_get_current_core() {
  sh_lapic_DEVICE *lapic_dev=sh_lapic_get_dev_apic_id(0);
  if (!lapic_dev) return -1;
  return (sh_int16)((sh_lapic_read(lapic_dev,SH_LAPIC_OFFSET_ID)>>24)&0xFF);
}
SH_STATUS sh_lapic_send_fixed_ipi(sh_lapic_DEVICE *lapic_dev,sh_uint8 vector,sh_uint32 destination_mode,sh_int16 target_lapic_id) {
  sh_uint32 icr_low=0;
  sh_uint32 icr_high=0;
  if (!lapic_dev) return SH_STATUS_INVALID_PARAMETER;
  if (vector<16) return SH_STATUS_INVALID_PARAMETER;
  switch (destination_mode) {
    case SH_LAPIC_IPI_DESTINATION_SPECIFIC:
      if (target_lapic_id==SH_LAPIC_IPI_NO_DESTINATION)
        return SH_STATUS_INVALID_PARAMETER;
      break;
    case SH_LAPIC_IPI_DESTINATION_SELF:
    case SH_LAPIC_IPI_DESTINATION_ALL:
    case SH_LAPIC_IPI_DESTINATION_ALL_EXCLUDING_SELF:
      if (target_lapic_id!=SH_LAPIC_IPI_NO_DESTINATION)
        return SH_STATUS_INVALID_PARAMETER;
      break;
    default:
      return SH_STATUS_INVALID_PARAMETER;
  }
  while (sh_lapic_ipi_is_busy(lapic_dev));
  if (destination_mode==SH_LAPIC_IPI_DESTINATION_SPECIFIC) icr_high=((sh_uint32)target_lapic_id&0xFF)<<24;
  icr_low=vector|(SH_LAPIC_IPI_TYPE_FIXED<<8)|(1u<<14)|destination_mode;
  sh_lapic_write(lapic_dev,SH_LAPIC_OFFSET_ICR_HIGH,icr_high);
  sh_lapic_write(lapic_dev,SH_LAPIC_OFFSET_ICR_LOW,icr_low);
  return SH_STATUS_SUCCESS;
}
SH_STATUS sh_lapic_send_ipi(sh_lapic_DEVICE *lapic_dev,sh_uint32 ipi_type,sh_uint32 destination_mode,sh_int16 target_lapic_id,sh_page_PHYSICAL_ADDRESS start_address) {
  sh_uint32 icr_low=0;
  sh_uint32 icr_high=0;
  if (!lapic_dev) return SH_STATUS_INVALID_PARAMETER;
  if (ipi_type!=SH_LAPIC_IPI_TYPE_INIT && ipi_type!=SH_LAPIC_IPI_TYPE_STARTUP && ipi_type!=SH_LAPIC_IPI_TYPE_NMI) return SH_STATUS_INVALID_PARAMETER;
  if (ipi_type==SH_LAPIC_IPI_TYPE_STARTUP) {
    if (start_address==0) return SH_STATUS_INVALID_PARAMETER;
    if (start_address&0xFFF) return SH_STATUS_INVALID_PARAMETER;
    if ((start_address>>12)>0xFF) return SH_STATUS_INVALID_PARAMETER;
  } else {
    if (start_address!=0) return SH_STATUS_INVALID_PARAMETER;
  }
  switch (destination_mode) {
    case SH_LAPIC_IPI_DESTINATION_SPECIFIC:
      if (target_lapic_id==SH_LAPIC_IPI_NO_DESTINATION)
        return SH_STATUS_INVALID_PARAMETER;
      break;
    case SH_LAPIC_IPI_DESTINATION_SELF:
    case SH_LAPIC_IPI_DESTINATION_ALL:
    case SH_LAPIC_IPI_DESTINATION_ALL_EXCLUDING_SELF:
      if (target_lapic_id!=SH_LAPIC_IPI_NO_DESTINATION)
        return SH_STATUS_INVALID_PARAMETER;
      break;
    default:
      return SH_STATUS_INVALID_PARAMETER;
  }
  while (sh_lapic_ipi_is_busy(lapic_dev));
  if (destination_mode==SH_LAPIC_IPI_DESTINATION_SPECIFIC) icr_high=((sh_uint32)target_lapic_id&0xFF)<<24;
  icr_low=(ipi_type<<8)|(1u<<14)|destination_mode;
  if (ipi_type==SH_LAPIC_IPI_TYPE_STARTUP) icr_low|=(sh_uint32)(start_address>>12);
  sh_lapic_write(lapic_dev,SH_LAPIC_OFFSET_ICR_HIGH,icr_high);
  sh_lapic_write(lapic_dev,SH_LAPIC_OFFSET_ICR_LOW,icr_low);
  return SH_STATUS_SUCCESS;
}
SH_STATUS sh_lapic_devs_query(char *sub_path,sh_devs_RESULT *result) {
  if (sub_path==SH_NULLPTR || result==SH_NULLPTR) return SH_STATUS_INVALID_PARAMETER;
  sh_uint64 path_len=sh_string_len(sub_path);
  if (path_len==0 || path_len>256) return SH_STATUS_INVALID_PARAMETER;
  if (lapic_devs.by_apic_id==SH_NULLPTR || lapic_devs.by_cpu_id==SH_NULLPTR) return SH_STATUS_DEVS_NOT_INITIALIZED;
  if (sh_string_compare(sub_path,"/count",6)) {
    if (path_len==6) {
      result->type=SH_DEVS_VALUE;
      result->value=lapic_devs.lapic_count;
      return SH_STATUS_SUCCESS;
    } else {
      return SH_STATUS_NOT_FOUND;
    }
  } else if (sh_string_compare(sub_path,"/timer-frequency",16)) {
    if (path_len==16) {
      result->type=SH_DEVS_VALUE;
      result->value=lapic_frequency;
      return SH_STATUS_SUCCESS;
    } else {
      return SH_STATUS_NOT_FOUND;
    }
  } else if (sh_string_compare(sub_path,"/max-apic-id",12)) {
    if (path_len==12) {
      result->type=SH_DEVS_VALUE;
      result->value=lapic_devs.max_apic_id;
      return SH_STATUS_SUCCESS;
    } else {
      return SH_STATUS_NOT_FOUND;
    }
  } else if (sh_string_compare(sub_path,"/max-acpi-id",12)) {
    if (path_len==12) {
      result->type=SH_DEVS_VALUE;
      result->value=lapic_devs.max_acpi_processor_id;
      return SH_STATUS_SUCCESS;
    } else {
      return SH_STATUS_NOT_FOUND;
    }
  } else if (sh_string_compare(sub_path,"/base-address",13)) {
    if (path_len==13) {
      result->type=SH_DEVS_VALUE;
      result->value=(sh_uint64)sh_lapic_get_dev_apic_id(sh_smp_gs_base()->lapic_id)->base;
      return SH_STATUS_SUCCESS;
    } else {
      return SH_STATUS_NOT_FOUND;
    }
  } else if (sh_string_compare(sub_path,"/this-cpu-lapic",15)) {
    if (path_len==15) {
      result->type=SH_DEVS_LAPIC;
      result->value=(sh_uint64)sh_lapic_get_dev_apic_id(sh_smp_gs_base()->lapic_id);
      return SH_STATUS_SUCCESS;
    } else {
      return SH_STATUS_NOT_FOUND;
    }
  } else if (sh_string_compare(sub_path,"/by-apic-id/",12)) {
    if (path_len>12 && path_len<=32) {
      sh_uint64 remaining_text=sh_string_len(sub_path)-sh_string_len("/by-apic-id/");
      char apic_id_str[22];
      sh_string_substring(sub_path,sh_string_len("/by-apic-id/"),remaining_text,apic_id_str);
      sh_uint64 apic_id=sh_string_to_uint64(apic_id_str);
      if (apic_id>lapic_devs.max_apic_id) return SH_STATUS_NOT_FOUND;
      result->type=SH_DEVS_LAPIC;
      result->value=(sh_uint64)lapic_devs.by_apic_id[apic_id];
      return SH_STATUS_SUCCESS;
    } else {
      return SH_STATUS_NOT_FOUND;
    }
  } else if (sh_string_compare(sub_path,"/by-acpi-id/",12)) {
    if (path_len>12 && path_len<=32) {
      sh_uint64 remaining_text=sh_string_len(sub_path)-sh_string_len("/by-acpi-id/");
      char acpi_id_str[22];
      sh_string_substring(sub_path,sh_string_len("/by-acpi-id/"),remaining_text,acpi_id_str);
      sh_uint64 acpi_id=sh_string_to_uint64(acpi_id_str);
      if (acpi_id>lapic_devs.max_apic_id) return SH_STATUS_NOT_FOUND;
      result->type=SH_DEVS_LAPIC;
      result->value=(sh_uint64)lapic_devs.by_cpu_id[acpi_id];
      return SH_STATUS_SUCCESS;
    } else {
      return SH_STATUS_NOT_FOUND;
    }
  } else {
    return SH_STATUS_NOT_FOUND;
  }
  return SH_STATUS_SUCCESS;
}
