#include "ios_kernel.h"
#include "ios_kernel_debug.h"
#include "ios_kernel_hardware.h"
#include "ios_kernel_heap.h"
#include "ios_kernel_ipc_thread.h"
#include "ios_kernel_otp.h"
#include "ios_kernel_process.h"
#include "ios_kernel_resourcemanager.h"
#include "ios_kernel_semaphore.h"
#include "ios_kernel_scheduler.h"
#include "ios_kernel_thread.h"
#include "ios_kernel_timer.h"

#include "ios/acp/ios_acp.h"
#include "ios/auxil/ios_auxil.h"
#include "ios/bsp/ios_bsp.h"
#include "ios/crypto/ios_crypto.h"
#include "ios/fpd/ios_fpd.h"
#include "ios/fs/ios_fs.h"
#include "ios/mcp/ios_mcp.h"
#include "ios/net/ios_net.h"
#include "ios/nim/ios_nim.h"
#include "ios/nsec/ios_nsec.h"
#include "ios/pad/ios_pad.h"
#include "ios/test/ios_test.h"
#include "ios/usb/ios_usb.h"

#include "ios/ios_enum.h"
#include "ios/ios_stackobject.h"

#include <common/log.h>
#include <libcpu/cpu_formatters.h>
#include <functional>

namespace ios::kernel
{

using namespace std::chrono_literals;
constexpr auto RootThreadNumMessages = 1u;
constexpr auto RootThreadStackSize = 0x2000u;
constexpr auto RootThreadPriority = 126u;

struct RootThreadMessage
{
   be2_val<RootThreadCommand> command;
   be2_array<uint32_t, 13> args;
};
CHECK_SIZE(RootThreadMessage, 0x38);

struct StaticKernelData
{
   be2_val<ThreadId> threadId;
   be2_val<TimerId> timerId;
   be2_val<MessageQueueId> messageQueueId;
   be2_array<Message, RootThreadNumMessages> messageBuffer;
   be2_array<uint8_t, RootThreadStackSize> threadStack;
   be2_struct<RootThreadMessage> rootTimerMessage;
   be2_struct<RootThreadMessage> sysprotEventMessage;
};

static phys_ptr<StaticKernelData>
sData;

struct ProcessInfo
{
   ProcessId pid;
   ThreadEntryFn entry;
   int32_t priority;
   uint32_t stackSize;
   uint32_t memPermMask;
   const char *threadName;
};

static ProcessInfo sProcessBootInfo[] = {
   { ProcessId::MCP,    mcp::processEntryPoint,       124, 0x2000,   0xC0030, "McpProcess" },
   { ProcessId::BSP,    bsp::processEntryPoint,       125, 0x1000,  0x100000, "BspProcess" },
   { ProcessId::CRYPTO, crypto::processEntryPoint,    123, 0x1000,   0xC0030, "CryptoProcess" },
   { ProcessId::USB,    usb::processEntryPoint,       107, 0x4000,   0x38600, "UsbProcess" },
   { ProcessId::FS,     fs::processEntryPoint,         85, 0x4000,  0x1C5870, "FsProcess" },
   { ProcessId::PAD,    pad::processEntryPoint,       117, 0x2000,    0x8180, "PadProcess" },
   { ProcessId::NET,    net::processEntryPoint,        80, 0x4000,    0x2000, "NetProcess" },
   { ProcessId::NIM,    nim::processEntryPoint,        50, 0x4000,         0, "NimProcess" },
   { ProcessId::NSEC,   nsec::processEntryPoint,       50, 0x1000,         0, "NsecProcess" },
   { ProcessId::FPD,    fpd::processEntryPoint,        50, 0x4000,         0, "FpdProcess" },
   { ProcessId::ACP,    acp::processEntryPoint,        50, 0x4000,         0, "AcpProcess" },
   { ProcessId::AUXIL,  auxil::processEntryPoint,      70, 0x4000,         0, "AuxilProcess" },
   { ProcessId::TEST,   test::processEntryPoint,       75, 0x2000,         0, "TestProcess" },
};

static Error
startProcesses(bool bootOnlyBSP)
{
   for (auto &info : sProcessBootInfo) {
      if (bootOnlyBSP) {
         if (info.pid != ProcessId::BSP) {
            continue;
         }
      } else if (info.pid == ProcessId::BSP) {
         continue;
      }

      auto stackPtr = allocProcessStatic(info.pid, info.stackSize, 0x10);
      auto error = IOS_CreateThread(info.entry,
                                    phys_cast<void *>(phys_addr { static_cast<uint32_t>(info.pid) }),
                                    phys_cast<uint8_t *>(stackPtr) + info.stackSize,
                                    info.stackSize,
                                    info.priority,
                                    ThreadFlags::AllocateTLS | ThreadFlags::Detached);
      if (error < Error::OK) {
         gLog->warn("Error creating process thread for pid {}, error = {}", info.pid, error);
         continue;
      }

      auto threadId = static_cast<ThreadId>(error);
      auto thread = internal::getThread(threadId);
      thread->pid = info.pid;
      internal::setThreadName(threadId, info.threadName);

      error = IOS_StartThread(threadId);
      if (error < Error::OK) {
         gLog->warn("Error starting process thread for pid {}, error = {}", info.pid, error);
         continue;
      }
   }

   return Error::OK;
}

static Error
initialiseRootThread()
{
   StackObject<Message> message;
   auto error = IOS_CreateMessageQueue(phys_addrof(sData->messageBuffer),
                                       static_cast<uint32_t>(sData->messageBuffer.size()));
   if (error < Error::OK) {
      gLog->error("Failed to create root thread message queue, error = {}", error);
      return error;
   }

   sData->messageQueueId = static_cast<MessageQueueId>(error);

   error = IOS_CreateTimer(5000us,
                           1000000us,
                           sData->messageQueueId,
                           makeMessage(phys_addrof(sData->rootTimerMessage)));
   if (error < Error::OK) {
      gLog->error("Failed to create root thread timer, error = {}", error);
      return error;
   }

   sData->timerId = static_cast<TimerId>(error);

   error = IOS_ReceiveMessage(sData->messageQueueId,
                              message,
                              MessageFlags::None);
   if (error < Error::OK) {
      gLog->error("Failed to receive root thread timer message, error = {}", error);
   }

   error = IOS_HandleEvent(DeviceId::SysProt,
                           sData->messageQueueId,
                           makeMessage(phys_addrof(sData->sysprotEventMessage)));
   if (error < Error::OK) {
      gLog->error("Failed to register sysprot event handler, error = {}", error);
      return error;
   }
   return IOS_ClearAndEnable(DeviceId::SysProt);
}

static Error
handleTimerEvent()
{
   return Error::OK;
}

static Error
handleSysprotEvent()
{
   return Error::OK;
}

static Error
kernelEntryPoint(phys_ptr<void> context)
{
   StackObject<Message> message;

   // Start timer thread
   internal::startTimerThread();

   // Set initial process caps
   internal::setSecurityLevel(SecurityLevel::Normal);
   internal::setClientCapability(ProcessId::KERNEL, FeatureId { 0x7FFFFFFF }, 0xFFFFFFFFu);
   internal::setClientCapability(ProcessId::MCP, FeatureId { 0x7FFFFFFF }, 0xFFFFFFFFu);
   internal::setClientCapability(ProcessId::BSP, FeatureId { 0x7FFFFFFF }, 0xFFFFFFFFu);

   for (auto i = +ProcessId::CRYPTO; i < NumIosProcess; ++i) {
      internal::setClientCapability(ProcessId { i }, FeatureId { 1 }, 0xF);
   }

   // Initialise shared heap
   auto error = IOS_CreateHeap(phys_cast<void *>(phys_addr { 0x1D000000 }),
                               0x2B00000);
   if (error < Error::OK) {
      gLog->error("Failed to create shared heap, error = {}", error);
      return error;
   }

   auto sharedHeapId = static_cast<HeapId>(error);
   if (sharedHeapId != 1) {
      gLog->error("Expected IOS kernel sharedHeapId to be 1, found {}", sharedHeapId);
      return Error::Invalid;
   }

   error = IOS_CreateCrossProcessHeap(0x20000);
   if (error < Error::OK) {
      gLog->error("Failed to create cross process heap, error = {}", error);
      return error;
   }

   // Start the BSP process
   error = startProcesses(true);
   if (error < Error::OK) {
      gLog->error("Failed to start BSP process, error = {}", error);
      return error;
   }

   // Wait for BSP to start
   IOS_SetThreadPriority(CurrentThread, 124);

   while (!internal::bspReady()) {
      IOS_YieldCurrentThread();
   }

   IOS_SetThreadPriority(CurrentThread, 126);

   // Initialise root kernel thread
   error = initialiseRootThread();
   if (error < Error::OK) {
      gLog->error("Failed to initialise root thread, error = {}", error);
      return error;
   }

   // Start the rest of the processes
   error = startProcesses(false);
   if (error < Error::OK) {
      gLog->error("Failed to start remaining IOS processes, error = {}", error);
      return error;
   }

   // Start IPC thread
   internal::startIpcThread();

   while (true) {
      error = IOS_ReceiveMessage(sData->messageQueueId, message, MessageFlags::None);
      if (error) {
         return error;
      }

      auto rootThreadMessage = parseMessage<RootThreadMessage>(message);
      switch (rootThreadMessage->command) {
      case RootThreadCommand::Timer:
         error = handleTimerEvent();
         break;
      case RootThreadCommand::SysprotEvent:
         error = handleSysprotEvent();
         break;
      default:
         gLog->warn("Received unexpected message on root thread, command = {}", rootThreadMessage->command);
      }
   }
}

Error
start()
{
   // Initialise static memory
   internal::initialiseProcessStaticAllocators();

   internal::initialiseStaticHardwareData();
   internal::initialiseStaticHeapData();
   internal::initialiseStaticSchedulerData();
   internal::initialiseStaticMessageQueueData();
   internal::initialiseStaticResourceManagerData();
   internal::initialiseStaticSemaphoreData();
   internal::initialiseStaticThreadData();
   internal::initialiseStaticTimerData();

   internal::initialiseOtp();

   sData = allocProcessStatic<StaticKernelData>();
   sData->rootTimerMessage.command = RootThreadCommand::Timer;
   sData->sysprotEventMessage.command = RootThreadCommand::SysprotEvent;

   // Create root kernel thread
   auto error = IOS_CreateThread(kernelEntryPoint,
                                 nullptr,
                                 phys_addrof(sData->threadStack) + sData->threadStack.size(),
                                 static_cast<uint32_t>(sData->threadStack.size()),
                                 RootThreadPriority,
                                 ThreadFlags::Detached);
   if (error < Error::OK) {
      return error;
   }

   // Force start the root kernel thread. We cannot use IOS_StartThread
   // because it reschedules and we are not running on an IOS thread.
   auto threadId = static_cast<ThreadId>(error);
   auto thread = internal::getThread(threadId);
   thread->state = ThreadState::Ready;
   internal::setThreadName(threadId, "KernelProcess");
   internal::queueThread(thread);

   // Send the power on interrupt
   internal::setInterruptAhbAll(AHBALL {}.PowerButton(true));

   // Start the host hardware thread
   internal::startHardwareThread();
   return Error::OK;
}

void
join()
{
   internal::joinHardwareThread();
}

void
stop()
{
   internal::stopHardwareThread();
}

} // namespace ios::kernel
