/// Thread.cpp
///
/// (c) 2002-2019 Bastian Spiegel <bs@tkscript.de> 
///     - Distributed under terms of the GNU General Public License (GPL).
///       See COPYING and <http://www.gnu.org/licenses/licenses.html#GPL> for further information.
///
///
/// changelog:
///      04Jul2002 <fli> file created
///      11Mar2005 source cleanup
///      15Mai2006 moved from sdl plugin to tks source, rewrote to use pthreads / win32api
///

// Define DX_WIN32_YIELD to include yield() support for win32 (default = off)
// Define DX_PTHREADP to include pthread_setaffinity support (needs glibc, default=off)
// Define DX_GETTID to include gettid() syscall support (linux, default = off)

#include "tks.h" 

#ifdef DX_PTHREADS
#include <sched.h>
#ifdef DX_PTHREADP
#include <pthreadP.h>
#endif
#endif

#ifdef YAC_LINUX
extern "C" {
#include <sys/types.h> 
#include <sys/sysinfo.h>
#include <linux/unistd.h> 
#include <errno.h> 
#include <unistd.h>
#include <syscall.h> 
#ifdef DX_GETTID
_syscall0(pid_t, gettid) 
 pid_t gettid(void); 
#endif
}
#endif

#include "TKS_Mutex.h" 
#include "TKS_Condition.h"

#include <stdio.h>
#include <stdlib.h>


#include "YAC_ValueObject.h"
#include "YAC_Event.h"
#include "TKS_Mailbox.h"
#include "TKS_Thread.h"
#include "TKS_ScriptObjectID.h"
#include "TKS_FunctionObject.h"


// // #if defined(YAC_WIN32) && defined(HAVE_WIN10)
// // #include <Processthreadsapi.h>
// // #if 0
// // YAC_APIC BOOL SetThreadSelectedCpuSets(
// //    _In_       HANDLE Thread,
// //    _In_ const ULONG  *CpuSetIds,
// //    _In_       ULONG  CpuSetIdCount
// //                                             );
// // #endif
// // #endif

#ifdef YAC_WIN32
typedef BOOL (*SetThreadSelectedCpuSets_fxn_t) (
   _In_       HANDLE Thread,
   _In_ const ULONG  *CpuSetIds,
   _In_       ULONG  CpuSetIdCount
                                                );
#endif // YAC_WIN32


#ifndef YAC_FORCE_NO_TLS
YAC_TLS TKS_Thread *tks_current_thread = NULL;

// This is used by plugins that create their own threads (i.e. NOT by using a Thread object)
//  and want to call script functions from within that thread.
//  After the plugin has created the thread, it must create a context and set this as the
//   default context (yacContextCreate(), yacContextSetDefault())
//
// If a thread was created using a Thread object, this pointer is NULL (tks_current_thread->context is used instead)
//
// Also see yacContextGetDefault()
YAC_TLS YAC_ContextHandle tks_current_context = NULL;
#endif // YAC_FORCE_NO_TLS


#ifdef DX_PTHREADS
static void        *tksvm__threadentry(void*_TKS_Thread) {
#else 
#ifdef YAC_WIN32
static DWORD WINAPI tksvm__threadentry(LPVOID _TKS_Thread) {
#else
   // no threads
static void *tksvm__threadentry(void*_TKS_Thread) {
#endif 
#endif
   TKS_Thread *thread=(TKS_Thread *)_TKS_Thread;

#ifndef YAC_FORCE_NO_TLS
   tks_current_thread = thread;
#endif // YAC_FORCE_NO_TLS

#ifdef YAC_LINUX
   thread->lwp_id = syscall(SYS_gettid);
#endif

#ifdef DX_PTHREADS
   thread->b_pthread_started = YAC_TRUE;
#endif

   yac_host->yacContextSetDefault(thread->context); // [25Jan2018] added

   YAC_Value args[1];
   args[0].initObject(thread, 0);
   yac_host->yacEvalFunction(thread->context, (void*)thread->tks_entry_fun, 1, args);

   // Print exception (if any)
   if(thread->context->b_exception_raised)
   {
      thread->context->printException();
   }

   thread->b_clean_exit = 1;

   // printf("xxx tksvm__threadentry: leaving thread id=%d\n", thread->_getId());

   return 0;
}



TKS_Thread::TKS_Thread(void) {
   b_created        = 0;
   b_clean_exit     = 0;
   b_running        = 0;
   b_killable       = 1;
   tks_entry_fun    = NULL;
   user_data        = NULL;
   priority         = TKS_THREAD_PRIORITY_NORMAL;
   context          = NULL;

#ifdef DX_PTHREADS
   b_pthread_started = 0;
#ifdef YAC_LINUX
   lwp_id = -1;
#endif
#endif

   yacmem thisAddr;
   thisAddr.any = this;
   rand_seed = 0x4489abcdu ^ thisAddr.ui;
}

TKS_Thread::~TKS_Thread() {
   _kill();
}

void TKS_Thread::initFromCurrent2(void) {
   //
   // Helper fxn to initialize Thread object from current thread environment  
   //
   b_created  = 1;
   b_running  = 1;
   b_killable = 0;
   context = NULL;

#ifdef DX_PTHREADS
   pthread_id = pthread_self();
#else
#ifdef YAC_WIN32
   dwThreadId = GetCurrentThreadId();
   hThread = GetCurrentThread();
#endif // YAC_WIN32
#endif // DX_PTHREAD
}

void TKS_Thread::initMain(void) {
   initFromCurrent2();
   context = (TKS_Context*) tkscript->main_context;
}

void TKS_Thread::initFromCurrent(void) {
   initFromCurrent2();
   context = (TKS_Context*) yac_host->yacContextCreate();
}

sSI TKS_Thread::_create(YAC_Object *_fun) {
   if((!b_created) && YAC_CHK(_fun, TKS_CLID_FUNCTION))
   {
      context = (TKS_Context*) yac_host->yacContextCreate();
      context->tks_fstackc--;
      context->tks_fstackc->initObject(this, 0); // First local var

      tks_entry_fun = (TKS_FunctionObject*) _fun; 
      {
         b_clean_exit = YAC_FALSE;
         b_running = YAC_TRUE;

#ifdef DX_PTHREADS
         b_created = (pthread_create( &pthread_id, NULL, tksvm__threadentry, (void*) this ) == 0);

#ifdef YAC_LINUX
         if(b_created)
         {
            /* wait for lwp_id field to become valid */
            while(!b_pthread_started)
            {
               pthread_yield();
            }
         }
#endif // YAC_LINUX

#else

#ifdef YAC_WIN32
         // (adapted from msdn example)
         hThread = CreateThread( 
            NULL,              // default security attributes
            0,                 // use default stack size  
            tksvm__threadentry,// thread function 
            (LPVOID)this,      // argument to thread function 
            0,                 // use default creation flags 
            &dwThreadId);      // returns the thread identifier
         b_created = (hThread != NULL);
#endif

#endif // DX_PTHREADS
      }

      if(!b_created)
      {
         yac_host->yacContextDestroy(context);
         context = NULL;
      }

      b_running = b_created;

      return b_created;
   }

   return 0;
}

void TKS_Thread::_kill(void) {
   if(b_created && b_killable)
   {
      if(YAC_FALSE == b_clean_exit)
      {
         Dprintf("[---] killing Thread id=%d leftovers..\n", _getId());

#ifdef DX_PTHREADS
         ///pthread_join( pthread_id, NULL);
         pthread_detach( pthread_id );
         pthread_cancel( pthread_id );
         pthread_id = 0;
#else
#ifdef YAC_WIN32
         SuspendThread( hThread );
         TerminateThread( hThread, 10 ); // 10 = exit code
         WaitForMultipleObjects(1, &hThread, TRUE, 5000 /*INFINITE*/); // wait max. 5sec
         CloseHandle(hThread);
         hThread = NULL;
#endif
#endif
      }
      else
      {
#ifdef DX_PTHREADS
         ///pthread_join( pthread_id, NULL);
         pthread_detach( pthread_id );
         pthread_cancel( pthread_id );
         pthread_id = 0;
#else
#ifdef YAC_WIN32
         SuspendThread( hThread );
         TerminateThread( hThread, 10 ); // 10 = exit code
         CloseHandle(hThread);
         hThread = NULL;
#endif
#endif
      }

      // Delete script execution context
      if(NULL != context)
      {
         yac_host->yacContextDestroy(context);
         context = NULL;
      }

      b_running = 0;
      b_created = 0;

#ifdef DX_PTHREADS
      b_pthread_started = 0;
#ifdef YAC_LINUX
      lwp_id = -1;
#endif
#endif
   }

   /*if(action_queue != NULL)
   {
      StaticList::Free(&action_queue);
   }*/
}

sSI TKS_Thread::_wait(void) {
   if(b_created && b_killable)
   {
      if(1 || (YAC_FALSE == b_clean_exit))
      {
         b_running = 0; // signalize thread cancellation
#ifdef DX_PTHREADS
         pthread_join( pthread_id, NULL ); // wait for thread to exit 
         pthread_id = 0;
#else
#ifdef YAC_WIN32
         WaitForMultipleObjects(1, &hThread, TRUE, INFINITE); // wait for thread to exit 
         CloseHandle(hThread);
         hThread = NULL;
#endif
#endif
      }
      else
      {
#ifdef DX_PTHREADS
         pthread_detach( pthread_id );
         pthread_cancel( pthread_id );
         pthread_id = 0;
#else
#ifdef YAC_WIN32
         SuspendThread( hThread );
         TerminateThread( hThread, 10 ); // 10 = exit code
         CloseHandle(hThread);
         hThread = NULL;
#endif
#endif
      }

      // Delete script execution context
      if(NULL != context)
      {
         yac_host->yacContextDestroy(context);
         context = NULL;
      }

      b_running = 0;
      b_created = 0;

#ifdef DX_PTHREADS
      b_pthread_started = 0;
#ifdef YAC_LINUX
      lwp_id = -1;
#endif
#endif
      return b_clean_exit;
   }

   return -1;
}

sSI TKS_Thread::_getId(void) {
   if(b_created)
   {

#ifdef YAC_LINUX
      return (sSI) lwp_id;
#else

#ifdef DX_PTHREADS
      return (sSI) pthread_id;
#else
#ifdef YAC_WIN32
      return (sSI) dwThreadId;
#endif
#endif

#endif // YAC_LINUX

   }
   return -1;
}

sSI TKS_Thread::_isRunning(void) {
   return b_running;
}

void TKS_Thread::_setName (YAC_String *_name) {
   name.yacCopy(_name);
}

void TKS_Thread::_getName (YAC_Value *_r) {
   _r->initNewString(&name);
}

void TKS_Thread::_setUserdata(YAC_Object *_o) {
   user_data = _o;
}

YAC_Object *TKS_Thread::_getUserdata(void) {
   return user_data;
}

#ifdef DX_PTHREADS
void TKS_Thread::setSchedFIFOPriority(sUI _prio) {
   struct sched_param param;
   memset((void*)&param, 0, sizeof(struct sched_param));

   param.sched_priority = _prio;

   int err = pthread_setschedparam(pthread_id, SCHED_FIFO, &param);

   if(0 != err)
   {
      Dprintf("[---] Thread<%d>::setPriority: pthread_setschedparam(SCHED_FIFO, %u) failed\n",
              _getId(),
              _prio
              );
   }
}
#endif // DX_PTHREADS

void TKS_Thread::_setPriority(sSI _priority) {
   if(_priority != priority)
   {
      if(b_created)
      {
#ifdef DX_PTHREADS
         switch(_priority)
         {
         case TKS_THREAD_PRIORITY_HIGHEST:
            setSchedFIFOPriority(40);
            break;

         case TKS_THREAD_PRIORITY_TIME_CRITICAL:
            setSchedFIFOPriority(80);
            break;

         case TKS_THREAD_PRIORITY_BELOW_NORMAL:
         case TKS_THREAD_PRIORITY_IDLE:
         case TKS_THREAD_PRIORITY_LOWEST:
         case TKS_THREAD_PRIORITY_NORMAL:
         case TKS_THREAD_PRIORITY_ABOVE_NORMAL:
            {
               struct sched_param param;
               memset((void*)&param, 0, sizeof(struct sched_param));
               param.sched_priority = _priority;
               pthread_setschedparam(pthread_id, SCHED_OTHER, &param);
               priority = _priority;
            }
            break;
         default:
            Dprintf("[---] Thread<%d>::setPriority: illegal priority (%i).\n", _getId(), _priority);
            break;
         }
#else
#ifdef YAC_WIN32
         switch(_priority)
         {
         case TKS_THREAD_PRIORITY_ABOVE_NORMAL:
         case TKS_THREAD_PRIORITY_BELOW_NORMAL:
         case TKS_THREAD_PRIORITY_HIGHEST:
         case TKS_THREAD_PRIORITY_IDLE:
         case TKS_THREAD_PRIORITY_LOWEST:
         case TKS_THREAD_PRIORITY_NORMAL:
         case TKS_THREAD_PRIORITY_TIME_CRITICAL:
            SetThreadPriority(hThread, _priority);
            priority = _priority;
            break;
         default:
            Dprintf("[---] Thread<%d>::setPriority: illegal priority (%i).\n", _getId(), _priority);
            break;
         }
#endif // YAC_WIN32
#endif // DX_PTHREADS
      } // if b_created
   }
}

sSI TKS_Thread::_getPriority(void) {
   return priority;
}

void TKS_Thread::_SetProcessPriorityClass(sSI _priorityClass) {
#ifdef YAC_WIN32
   BOOL r = 0;
   DWORD dwExpectClass = 0;
   switch(_priorityClass)
   {
      case TKS_PROCESS_PRIORITY_CLASS_IDLE:
         dwExpectClass = IDLE_PRIORITY_CLASS;
         r = ::SetPriorityClass(GetCurrentProcess(), dwExpectClass);
         break;

      default:
      case TKS_PROCESS_PRIORITY_CLASS_NORMAL:
         dwExpectClass = NORMAL_PRIORITY_CLASS;
         r = ::SetPriorityClass(GetCurrentProcess(), dwExpectClass);
         break;

      case TKS_PROCESS_PRIORITY_CLASS_HIGH:
         dwExpectClass = HIGH_PRIORITY_CLASS;
         r = ::SetPriorityClass(GetCurrentProcess(), dwExpectClass);
         break;

      case TKS_PROCESS_PRIORITY_CLASS_REALTIME:
         // (note) won't when called with regular user privileges
         // (note) workaround: use taskmanager to manually set realtime priority
         // (note) OR: run process as administrator
         dwExpectClass = REALTIME_PRIORITY_CLASS;
         r = ::SetPriorityClass(GetCurrentProcess(), dwExpectClass);
         break;
   }

   if(0 == r)
   {
      Dprintf("[---] Thread::SetProcessPriorityClass<win32>: FAILED. r=%d GetLastError()=%d\n", r, GetLastError());
   }
   else
   {
      Dprintf("[...] Thread::SetProcessPriorityClass<win32>: ok, windows priority class is now 0x%08x (expect 0x%08x)\n", ::GetPriorityClass(GetCurrentProcess()), dwExpectClass);
   }

#else
   (void)_priorityClass;
#endif // YAC_WIN32
}

sBool TKS_Thread::_SetProcessAffinityMask32(sUI _mask) {
#ifdef YAC_WIN32
   DWORD_PTR procMask = 0;
   DWORD_PTR sysMask = 0;
   ::GetProcessAffinityMask(::GetCurrentProcess(), &procMask, &sysMask);
   DWORD_PTR newMask = ((DWORD_PTR)_mask) & sysMask;
   BOOL ret = ::SetProcessAffinityMask(::GetCurrentProcess(), newMask);
   if(!ret)
   {
      Dprintf("[~~~] Thread::SetProcessAffinityMask32: SetProcessAffinityMask() failed. GetLastError()=%d\n", GetLastError());
   }
   else
   {
      return YAC_TRUE;
   }
#endif // YAC_WIN32
   return YAC_FALSE;
}

sUI TKS_Thread::_GetMaxAffinityMask32(void) {
   sUI r = 1u;
#ifdef YAC_WIN32
   SYSTEM_LOGICAL_PROCESSOR_INFORMATION *procInfo;
   DWORD retLen = 0;////sizeof(SYSTEM_LOGICAL_PROCESSOR_INFORMATION);
   ::GetLogicalProcessorInformation(NULL, &retLen);
   // yac_host->printf("xxx retLen=%u\n", retLen);
   if(ERROR_INSUFFICIENT_BUFFER == GetLastError())
   {
      procInfo = (SYSTEM_LOGICAL_PROCESSOR_INFORMATION *)::malloc(retLen);
      if(NULL != procInfo)
      {
         if(::GetLogicalProcessorInformation(procInfo, &retLen))
         {
            sUI numProc = retLen / sizeof(SYSTEM_LOGICAL_PROCESSOR_INFORMATION);
            for(sUI procIdx = 0u; procIdx < numProc; procIdx++)
            {
               r |= sUI(procInfo[procIdx].ProcessorMask);  // truncate to 32 bit
            }
         }
         else
         {
            yac_host->printf("[~~~] Thread::GetMaxAffinityMask32: GetLogicalProcessorInformation() failed.\n");
         }
         ::free(procInfo);
      }
      else
      {
         yac_host->printf("[~~~] Thread::GetMaxAffinityMask32: failed to allocate %u procInfo bytes\n", retLen);
      }
   }
   else
   {
      yac_host->printf("[~~~] Thread::GetMaxAffinityMask32: GetLogicalProcessorInformation() did not return alloc size.\n");
   }
#elif defined(YAC_LINUX)
   int numProc = get_nprocs();
   if(numProc < 1)
      numProc = 1;
   if(numProc > 31)
   {
      r = ~0u;  // truncate to 32 bit
   }
   else
   {
      r = (1 << sUI(numProc)) - 1u;
   }
#endif
   return r;
}

void TKS_Thread::_setAffinityMask32(sSI _mask) {
   if(b_created)
   {
#ifdef DX_PTHREADS

#ifdef DX_PTHREADP
      sUI mask = (sUI) _mask;
      pthread_setaffinity_np(pthread_id, (const cpu_set_t*) &mask);
#else
#ifdef YAC_LINUX
      ///struct pthread *pd = (struct pthread *) pthread_id;
#ifdef DX_GETTID
      sched_setaffinity(gettid()/*pd->tid*/, sizeof(sSI), (sUI*) &_mask);
#endif
#endif
#endif // if DX_PTHREADP

#else // if DX_PTHREADS
#ifdef YAC_WIN32
      SetThreadAffinityMask(hThread, (DWORD) _mask);
#endif // YAC_WIN32
#endif // DX_PTHREADS
   } // if b_created
}

void TKS_Thread::_setCPUCore(sSI _coreIdx) {

   _setAffinityMask32(1u << sUI(_coreIdx));

#ifdef YAC_WIN32
   ::SetThreadPriorityBoost(hThread, TRUE/*disable*/);

   // (note) resolve fxn dynamically to maintain Windows 7/8 compatibility
#ifdef TKS_MT
   tkscript->mtx_global->lock();
#endif //TKS_MT

   static sBool bFxnCheck = YAC_FALSE;
   static SetThreadSelectedCpuSets_fxn_t SetThreadSelectedCpuSets_fxn = NULL;

   if(YAC_FALSE == bFxnCheck)
   {
      bFxnCheck = YAC_TRUE;
      HMODULE hDLL = ::LoadLibrary("Kernel32.dll");
      if(NULL != hDLL)
      {
         SetThreadSelectedCpuSets_fxn = (SetThreadSelectedCpuSets_fxn_t)
            ::GetProcAddress(hDLL, "SetThreadSelectedCpuSets");
      }
   }

#ifdef TKS_MT
   tkscript->mtx_global->unlock();
#endif //TKS_MT

   // tkscript->printf("xxx call SetThreadSelectedCpuSets_fxn=%p\n", SetThreadSelectedCpuSets_fxn);

   if(NULL != SetThreadSelectedCpuSets_fxn)
   {
      if(_coreIdx >= 0)
      {
         // "Sets the selected CPU Sets assignment for the specified thread.
         //  This assignment overrides the process default assignment, if one is set."
         //   -- https://msdn.microsoft.com/en-us/library/windows/desktop/mt186428(v=vs.85).aspx
         ULONG cpuSetIds = sUI(_coreIdx);
         SetThreadSelectedCpuSets_fxn(hThread,
                                      &cpuSetIds,
                                      1
                                      );
      }
      else
      {
         // Reset
         SetThreadSelectedCpuSets_fxn(hThread, NULL, 0);
      }
   }
#endif // YAC_WIN32
}

void TKS_Thread::_allocEventQueue(sSI _size) {
   mbox.allocEventQueue((sUI)_size);
}

sSI TKS_Thread::_sendEvent(YAC_Object *_ev) {
   if(YAC_BCHK(_ev, YAC_CLID_EVENT))
   {
      return mbox.sendEvent((YAC_Event*)_ev);
   }
   else
   {
      Dprintf("[---] Thread::sendEvent: not an Event object.\n");
      return ~0;
   }
}

YAC_Object *TKS_Thread::_peekEvent(void) {
   return mbox.peekEvent();
}

YAC_Object *TKS_Thread::_peekEventById(sSI _id) {
   return mbox.peekEventById(_id);
}

void TKS_Thread::_waitEvent(sSI _timeout, YAC_Value *_r) {
   mbox.waitEvent((sUI)_timeout, _r);
}

void TKS_Thread::_waitEventById(sSI _id, sSI _timeout, YAC_Value *_r) {
   mbox.waitEventById(_id, (sUI)_timeout, _r);
}

sUI TKS_Thread::_rand(void) {
   // yeah, this is a pretty lousy random fxn but I only needed it for testing
   rand_seed *= 16807u;
   return rand_seed;
}

void TKS_Thread::_srand(sUI _seed) {
   rand_seed = _seed;
}
