#include <Arduino.h>

#include "TeensyThreads.h"

volatile int p1 = 0;
volatile int p2 = 0;
volatile int p3 = 0;
volatile int p4 = 0;

void my_priv_func1(int data){
  p1 = 0;
  data *= 1000; // convert sec to ms;
  int mx = millis();
  while(1) {
    p1++;
    if ((int)millis() - mx > data) break;
  }
}

void my_priv_func2() {
  p2 = 0;
  while(1) p2++;
}

void my_priv_func3() {
  p3 = 0;
  while(1) p3++;
}

void my_priv_func_lock(void *lock) {
  Threads::Mutex *m = (Threads::Mutex *) lock;
  p4 = 0;
  m->lock();
  uint32_t mx = millis();
  while(millis() - mx < 500) p1++;
  m->unlock();
}

Threads::Mutex count_lock;
volatile int count1 = 0;
volatile int count2 = 0;
volatile int count3 = 0;

void lock_test1() {
  while(1) {
    count_lock.lock();
    for(int i=0; i<500; i++) count1++;
    count_lock.unlock();
  }
}

void lock_test2() {
  while(1) {
    count_lock.lock();
    for(int i=0; i<1000; i++) count2++;
    count_lock.unlock();
  }
}

void lock_test3() {
  while(1) {
    count_lock.lock();
    for(int i=0; i<100; i++) count3++;
    count_lock.unlock();
  }
}

int ratio_test(int a, int b, float r) {
  float f = (float)a / (float)b;
  if (a < b) f = 1.0/f;
  if (f > r) return 1;
  return 0;
}


void showp() {
  Serial.print(p1);
  Serial.print(" ");
  Serial.print(p2);
  Serial.print(" ");
  Serial.print(p3);
  Serial.println();
}

int id1, id2, id3;

void delay2(uint32_t ms)
{
  int mx = millis();
  while(millis() - mx < ms);
}

#define delayx delay


class subtest {
public:
int value;
void h(int x) { value = x; }
int test(Threads::Mutex *lk) { return lk->getState(); }
int getValue() { return value; }
} subinst;

class WireTest {
public:
bool beginTransaction() { return 1; }
bool endTransaction() { return 1; }
bool other() { return 1; }
};

int stack_fault = 0;
int stack_id = 0;

void stack_overflow_isr(void) {
  stack_fault = 1;
  threads.kill(threads.id());
}

// turn off optimizations or the optimizer will remove the recursion because
// it is smart enough to know it doesn't do anything
void __attribute__((optimize("O0"))) recursive_thread(int level) {
  if (stack_fault) return;
  char x[128]; // use up some stack space
  delay(x[0] = 20); // "use"  x[0] to avoid "unused variable" warning
  recursive_thread(level+1);
}

void runtest() {
  int save_p;
  int save_time;
  float rate;

  // benchmark with no threading
  my_priv_func1(1);
  save_time = p1;

  Serial.print("CPU speed consistency ");
  my_priv_func1(1);
  rate = (float)p1 / (float)save_time;
  if (rate < 1.2 && rate > 0.8) Serial.println("OK");
  else Serial.println("***FAIL***");

  Serial.print("Test thread start ");
  id1 = threads.addThread(my_priv_func1, 1);
  delayx(300);
  if (p1 != 0) Serial.println("OK");
  else Serial.println("***FAIL***");

  Serial.print("Test thread run state ");
  if (threads.getState(id1) == Threads::RUNNING) Serial.println("OK");
  else Serial.println("***FAIL***");

  Serial.print("Test thread return ");
  delayx(1000);
  save_p = p1;
  delayx(300);
  if (p1 != 0 && p1 == save_p) Serial.println("OK");
  else Serial.println("***FAIL***");

  Serial.print("Test thread speed ");
  rate = (float)p1 / (float)save_time;
  if (rate < 0.7 && rate > 0.3) Serial.println("OK");
  else Serial.println("***FAIL***");

  Serial.print("Speed no threads: ");
  Serial.println(save_time);
  Serial.print("Speed 1 thread: ");
  Serial.println(p1);
  Serial.print("Ratio: ");
  Serial.println(rate);

  Serial.print("Test set time slice ");
  id1 = threads.addThread(my_priv_func1, 1);
  delayx(2000);
  save_p = p1;
  id1 = threads.addThread(my_priv_func1, 1);
  threads.setTimeSlice(id1, 200);
  delayx(2000);
  float expected = (float)save_p * 2.0*200.0 / ((float)threads.DEFAULT_TICKS + 200.0);
  rate = (float)p1 / (float)expected;
  if (rate > 0.9 && rate < 1.1) Serial.println("OK");
  else Serial.println("***FAIL***");

  Serial.print("Speed default ticks: ");
  Serial.println(save_p);
  Serial.print("Speed 200 ticks: ");
  Serial.println(p1);
  Serial.print("Expected: ");
  Serial.println(expected);
  Serial.print("Ratio with expected: ");
  Serial.println(rate);

  Serial.print("Test delay yield ");
  id1 = threads.addThread(my_priv_func1, 1);
  threads.delay(1100);
  rate = (float)p1 / (float)save_time;
  if (rate > 0.7 && rate < 1.4) Serial.println("OK");
  else Serial.println("***FAIL***");

  Serial.print("Yield wait ratio: ");
  Serial.println(rate);

  Serial.print("Test thread end state ");
  if (threads.getState(id1) == Threads::ENDED) Serial.println("OK");
  else Serial.println("***FAIL***");

  Serial.print("Test thread reinitialize ");
  p2 = 0;
  id2 = threads.addThread(my_priv_func2);
  delayx(200);
  if (p2 != 0) Serial.println("OK");
  else Serial.println("***FAIL***");

  Serial.print("Test stack usage ");
  int sz = threads.getStackUsed(id2);
  // Seria.println(sz);
  if (sz>=40 && sz<=48) Serial.println("OK");
  else Serial.println("***FAIL***");

  Serial.print("Test thread suspend ");
  delayx(200);
  threads.suspend(id2);
  delayx(200);
  save_p = p2;
  delayx(200);
  if (p2 != 0 && p2 == save_p) Serial.println("OK");
  else Serial.println("***FAIL***");

  Serial.print("Test thread restart ");
  p2 = 0;
  threads.restart(id2);
  delayx(1000);
  if (p2 != 0) Serial.println("OK");
  else Serial.println("***FAIL***");

  Serial.print("Test thread stop ");
  threads.stop();
  delayx(200);
  p2 = 0;
  delayx(200);
  if (p2 == 0) Serial.println("OK");
  else Serial.println("***FAIL***");

  Serial.print("Test thread start ");
  threads.start();
  delayx(200);
  if (p2 != 0) Serial.println("OK");
  else Serial.println("***FAIL***");

  Serial.print("Test thread wait ");
  id3 = threads.addThread(my_priv_func1, 1);
  int time = millis();
  int r = threads.wait(id3);
  delayx(100);
  time = millis() - time;
  if (r==id3) Serial.println("OK");
  else Serial.println("***FAIL***");

  Serial.print("Test thread wait time ");
  if (time > 1000 && time < 2000) Serial.println("OK");
  else Serial.println("***FAIL***");

  Serial.print("Test thread kill ");
  id3 = threads.addThread(my_priv_func1, 2);
  delayx(300);
  threads.kill(id3);
  delayx(300);
  save_p = p1;
  delayx(300);
  if (save_p==p1) Serial.println("OK");
  else Serial.println("***FAIL***");

  delayx(1000);

  Serial.print("Test std::thread scope ");
  {
    std::thread th2(my_priv_func3);
    delayx(500);
  }
  delayx(500);
  save_p = p3;
  delayx(500);
  if (p3 != 0 && p3 == save_p) Serial.println("OK");
  else Serial.println("***FAIL***");

  Serial.print("Test basic lock ");
  id1 = threads.addThread(my_priv_func1, 2);
  delayx(500);
  {
    Threads::Suspend lock;
    save_p = p1;
    delayx(500);
    if (save_p == p1) Serial.println("OK");
    else Serial.println("***FAIL***");
  }

  Serial.print("Test basic unlock ");
  delayx(500);
  if (save_p != p1) Serial.println("OK");
  else Serial.println("***FAIL***");


  Serial.print("Test mutex lock state ");
  Threads::Mutex mx;
  mx.lock();
  r = mx.try_lock();
  if (r == 0) Serial.println("OK");
  else Serial.println("***FAIL***");

  Serial.print("Test mutex lock thread ");
  id1 = threads.addThread(my_priv_func_lock, &mx);
  delayx(200);
  if (p4 == 0) Serial.println("OK");
  else Serial.println("***FAIL***");

  Serial.print("Test mutex unlock ");
  mx.unlock();
  delayx(500);
  if (p1 != 0) Serial.println("OK");
  else Serial.println("***FAIL***");

  Serial.print("Test fast locks ");
  id1 = threads.addThread(lock_test1);
  id2 = threads.addThread(lock_test2);
  id3 = threads.addThread(lock_test3);
  delayx(3000);
  threads.kill(id1);
  threads.kill(id2);
  threads.kill(id3);
  if (ratio_test(count1, count2, 1.2)) Serial.println("***FAIL***");
  //else if (ratio_test(count1, count3, 1.2)) Serial.println("***FAIL***");
  else Serial.println("OK");

  Serial.print(count1);
  Serial.print(" ");
  Serial.print(count2);
  Serial.print(" ");
  Serial.print(count3);
  Serial.println();

  Serial.print("Test std::mutex lock ");
  std::mutex g_mutex;
  {
    std::lock_guard<std::mutex> lock(g_mutex);
    if (g_mutex.try_lock() == 0) Serial.println("OK");
    else Serial.println("***FAIL***");
  }

  Serial.print("Test std::mutex unlock ");
  if (g_mutex.try_lock() == 1) Serial.println("OK");
  else Serial.println("***FAIL***");
  g_mutex.unlock();

  Serial.print("Test Grab init ");
  subinst.h(10);
  ThreadWrap(subinst, sub2);
  #define subinst ThreadClone(sub2)
  if(subinst.getValue() == 10) Serial.println("OK");
  else Serial.println("***FAIL***");

  Serial.print("Test Grab set ");
  subinst.h(25);
  if(subinst.getValue() == 25) Serial.println("OK");
  else Serial.println("***FAIL***");

  Serial.print("Test Grab lock ");
  if (subinst.test(&(sub2.getLock())) == 1) Serial.println("OK");
  else Serial.println("***FAIL***");

  Serial.print("Test thread stack overflow ");
  uint8_t *mstack = new uint8_t[1024];
  stack_id = threads.addThread(recursive_thread, 0, 512, mstack+512);
  threads.delay(2000);
  threads.kill(stack_id);
  if (stack_fault) Serial.println("OK");
  else Serial.println("***FAIL***");
  delete[] mstack;
}

void runloop() {
  static int timeloop = millis();
  static int mx = 0;
  static int count = 0;
  if (millis() - mx > 5000) {
    Serial.print(count);
    Serial.print(": ");
    Serial.print((millis() - timeloop)/1000);
    Serial.print(" sec ");
    showp();
    count++;
    mx = millis();
  }
}

void setup() {
  delay(1000);
  runtest();
  Serial.println("Test infinite loop (will not end)");
}

void loop() {
  runloop();
}
