/**
 * @file main
 *
 */

/*********************
 *      INCLUDES
 *********************/
#define _DEFAULT_SOURCE /* needed for usleep() */
#include <stdlib.h>
#include <unistd.h>
#define SDL_MAIN_HANDLED /*To fix SDL's "undefined reference to WinMain" issue*/
#include <SDL2/SDL.h>
#include "lvgl/lvgl.h"
//#include "lvgl/examples/lv_examples.h"
//#include "lv_demos/lv_demo.h"
#include "lv_drivers/display/monitor.h"
#include "lv_drivers/indev/mouse.h"
#include "lv_drivers/indev/keyboard.h"
#include "lv_drivers/indev/mousewheel.h"

// get PineTime header
#include "displayapp/InfiniTimeTheme.h"
#include <drivers/Hrs3300.h>
#include <drivers/Bma421.h>

#include "BootloaderVersion.h"
#include "components/battery/BatteryController.h"
#include "components/ble/BleController.h"
#include "components/ble/NotificationManager.h"
#include "components/brightness/BrightnessController.h"
#include "components/motor/MotorController.h"
#include "components/datetime/DateTimeController.h"
#include "components/heartrate/HeartRateController.h"
#include "components/fs/FS.h"
#include "drivers/Spi.h"
#include "drivers/SpiMaster.h"
#include "drivers/SpiNorFlash.h"
#include "drivers/St7789.h"
#include "drivers/TwiMaster.h"
#include "drivers/Cst816s.h"
#include "drivers/PinMap.h"
#include "systemtask/SystemTask.h"
#include "drivers/PinMap.h"
#include "touchhandler/TouchHandler.h"
#include "buttonhandler/ButtonHandler.h"

// get the simulator-headers
#include "displayapp/DisplayApp.h"
#include "displayapp/LittleVgl.h"

#include <nrfx_gpiote.h>
#include <hal/nrf_gpio.h>
#include <mdk/nrf52.h> // initialize NRF_WDT and NRF_POWER

#include <iostream>
#include <typeinfo>
#include <algorithm>
#include <cmath> // std::pow

// additional includes for 'saveScreenshot()' function
#include <iomanip> // put_time
#include <sstream>
#include <chrono>
#include <ctime>   // localtime
#if defined(WITH_PNG)
#include <png.h>
#endif
#include <gif.h>

#include "img/sim_background.h" // provides variable SIM_BACKGROUND

/*********************
 *      DEFINES
 *********************/

/**********************
 *      TYPEDEFS
 **********************/
// copied from lv_drivers/display/monitor.c to get the SDL_Window for the InfiniTime screen
extern "C"
{
typedef struct {
  SDL_Window * window;
  SDL_Renderer * renderer;
  SDL_Texture * texture;
  volatile bool sdl_refr_qry;
#if MONITOR_DOUBLE_BUFFERED
  uint32_t * tft_fb_act;
#else
  uint32_t tft_fb[LV_HOR_RES_MAX * LV_VER_RES_MAX];
#endif
}monitor_t;
extern monitor_t monitor;
}

void saveScreenshot()
{
  auto now = std::chrono::system_clock::now();
  auto in_time_t = std::chrono::system_clock::to_time_t(now);
  // timestamped png filename
  std::stringstream ss;
  ss << "InfiniSim_" << std::put_time(std::localtime(&in_time_t), "%F_%H%M%S");
  std::string screenshot_filename_base = ss.str();
  // TODO: use std::format once we have C++20 and new enough GCC 13
  //std::string screenshot_filename_base = std::format("InfiniSim_%F_%H%M%S", std::chrono::floor<std::chrono::seconds>(now));
  //std::string screenshot_filename_base = "InfiniSim";

  const int width = 240;
  const int height = 240;
  auto renderer = monitor.renderer;


#if defined(WITH_PNG)
  std::string screenshot_filename = screenshot_filename_base + ".png";

  FILE * fp2 = fopen(screenshot_filename.c_str(), "wb");
  if (!fp2) {
    // dealing with error
    return;
  }
  // 1. Create png struct pointer
  png_structp png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
  if (!png_ptr){
      // dealing with error
  }
  png_infop info_ptr = png_create_info_struct(png_ptr);
  if (!info_ptr) {
      png_destroy_write_struct(&png_ptr, (png_infopp)NULL);
      // dealing with error
  }
  int bit_depth = 8;
  png_init_io(png_ptr, fp2);
  png_set_IHDR(png_ptr, info_ptr, width, height, bit_depth, \
      PNG_COLOR_TYPE_RGBA, PNG_INTERLACE_NONE, \
      PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT);

  // 3. Convert 1d array to 2d array to be suitable for png struct
  //    I assumed the original array is 1d
  std::array<png_bytep, 240> row_pointers;
  //png_bytepp row_pointers = (png_bytepp)png_malloc(png_ptr, sizeof(png_bytep) * height);
  for (int i = 0; i < height; i++) {
      row_pointers[i] = (png_bytep)png_malloc(png_ptr, width*4);
  }
  constexpr size_t zoom = MONITOR_ZOOM;
  const Uint32 format = SDL_PIXELFORMAT_RGBA8888;
  SDL_Surface *surface = SDL_CreateRGBSurfaceWithFormat(0, width*zoom, height*zoom, 32, format);
  SDL_RenderReadPixels(renderer, NULL, format, surface->pixels, surface->pitch);
  png_bytep pixels = (png_bytep)surface->pixels;
  for (int hi = 0; hi < height; hi++) {
    for (int wi = 0; wi < width; wi++) {
      int c = wi * 4;
      row_pointers.at(hi)[wi*4+0] = pixels[hi*surface->pitch*zoom + wi*4*zoom + 3]; // red
      row_pointers.at(hi)[wi*4+1] = pixels[hi*surface->pitch*zoom + wi*4*zoom + 2]; // greeen
      row_pointers.at(hi)[wi*4+2] = pixels[hi*surface->pitch*zoom + wi*4*zoom + 1]; // blue
      row_pointers.at(hi)[wi*4+3] = 255; // alpha
    }
  }

  // 4. Write png file
  png_write_info(png_ptr, info_ptr);
  png_write_image(png_ptr, row_pointers.data());
  png_write_end(png_ptr, info_ptr);
  png_destroy_write_struct(&png_ptr, &info_ptr);
  fclose(fp2);

  SDL_FreeSurface(surface);
#else
  std::string screenshot_filename = screenshot_filename_base + ".bmp";
  const Uint32 format = SDL_PIXELFORMAT_RGB888;
  SDL_Surface *surface = SDL_CreateRGBSurfaceWithFormat(0, width, height, 24, format);
  SDL_RenderReadPixels(renderer, NULL, format, surface->pixels, surface->pitch);
  SDL_SaveBMP(surface, screenshot_filename.c_str());
  SDL_FreeSurface(surface);
#endif
  std::cout << "InfiniSim: Screenshot created: " << screenshot_filename << std::endl;
}

class GifManager
{
private:
  GifWriter writer = {};
  std::chrono::system_clock::time_point last_frame;
  bool in_progress = false;
  static constexpr uint32_t delay_ds = 100/20; // in 1/100 s, so 1 ds = 10 ms
  static constexpr int sdl_width = 240;
  static constexpr int sdl_height = 240;

public:
  GifManager()
  {}
  ~GifManager()
  {
    if (in_progress) {
      close();
    }
  }

  bool is_in_progress() const
  {
    return in_progress;
  }
  void create_new()
  {
    assert(!in_progress);
    auto now = std::chrono::system_clock::now();
    auto in_time_t = std::chrono::system_clock::to_time_t(now);
    // timestamped png filename
    std::stringstream ss;
    ss << "InfiniSim_" << std::put_time(std::localtime(&in_time_t), "%F_%H%M%S");
    std::string screenshot_filename_base = ss.str();
    // TODO: use std::format once we have C++20 and new enough GCC 13
    //std::string screenshot_filename_base = std::format("InfiniSim_%F_%H%M%S", std::chrono::floor<std::chrono::seconds>(now));
    std::string screenshot_filename = screenshot_filename_base + ".gif";
    std::cout << "InfiniSim: Screen-capture started: " << screenshot_filename << std::endl;
    GifBegin( &writer, screenshot_filename.c_str(), sdl_width, sdl_height, delay_ds, 8, true );
    in_progress = true;
    write_frame(true);
  }
  void write_frame(bool force = false)
  {
    assert(in_progress);
    auto now = std::chrono::system_clock::now();
    if (force || ((now - last_frame) > std::chrono::milliseconds(delay_ds*10)) )
    {
      last_frame = std::chrono::system_clock::now();
      auto renderer = monitor.renderer;
      constexpr size_t zoom = MONITOR_ZOOM;
      const Uint32 format = SDL_PIXELFORMAT_RGBA8888;
      SDL_Surface *surface = SDL_CreateRGBSurfaceWithFormat(0, sdl_width*zoom, sdl_height*zoom, 32, format);
      SDL_RenderReadPixels(renderer, NULL, format, surface->pixels, surface->pitch);
      uint8_t *pixels = (uint8_t*) surface->pixels;

      std::array<uint8_t, 240*240*4> image;
      for (int hi = 0; hi < sdl_height; hi++) {
        for (int wi = 0; wi < sdl_width; wi++) {
          auto red   = pixels[hi*surface->pitch*zoom + wi*4*zoom + 3]; // red
          auto green = pixels[hi*surface->pitch*zoom + wi*4*zoom + 2]; // green
          auto blue  = pixels[hi*surface->pitch*zoom + wi*4*zoom + 1]; // blue
          image[(hi * sdl_width + wi)*4 + 0] = red;
          image[(hi * sdl_width + wi)*4 + 1] = green;
          image[(hi * sdl_width + wi)*4 + 2] = blue;
          image[(hi * sdl_width + wi)*4 + 3] = 255; // no alpha
        }
      }
      GifWriteFrame(&writer, image.data(), sdl_width, sdl_height, delay_ds, 8, false);
    }
  }
  void close()
  {
    assert(in_progress);
    in_progress = false;
    GifEnd(&writer);
    std::cout << "InfiniSim: Screen-capture finished" << std::endl;
  }
};

/**********************
 *  STATIC PROTOTYPES
 **********************/
static void hal_init(void);
static int tick_thread(void *data);

/**********************
 *  STATIC VARIABLES
 **********************/
lv_indev_t *kb_indev;
lv_indev_t *mouse_indev = nullptr;

/**********************
 *      MACROS
 **********************/

/**********************
 *   GLOBAL FUNCTIONS
 **********************/

void nrfx_gpiote_evt_handler(nrfx_gpiote_pin_t pin, nrf_gpiote_polarity_t action) {}

/*********************
 *      DEFINES
 *********************/

/**********************
 *      TYPEDEFS
 **********************/

/**********************
 *      VARIABLES
 **********************/

/**********************
 *  STATIC PROTOTYPES
 **********************/

/**********************
 *   GLOBAL FUNCTIONS
 **********************/
constexpr NRF_TWIM_Type *NRF_TWIM1 = nullptr;


static constexpr uint8_t touchPanelTwiAddress = 0x15;
static constexpr uint8_t motionSensorTwiAddress = 0x18;
static constexpr uint8_t heartRateSensorTwiAddress = 0x44;

Pinetime::Drivers::SpiMaster spi {Pinetime::Drivers::SpiMaster::SpiModule::SPI0,
                                  {Pinetime::Drivers::SpiMaster::BitOrder::Msb_Lsb,
                                   Pinetime::Drivers::SpiMaster::Modes::Mode3,
                                   Pinetime::Drivers::SpiMaster::Frequencies::Freq8Mhz,
                                   Pinetime::PinMap::SpiSck,
                                   Pinetime::PinMap::SpiMosi,
                                   Pinetime::PinMap::SpiMiso}};

Pinetime::Drivers::Spi lcdSpi {spi, Pinetime::PinMap::SpiLcdCsn};
Pinetime::Drivers::St7789 lcd {lcdSpi, Pinetime::PinMap::LcdDataCommand, Pinetime::PinMap::LcdReset};

Pinetime::Drivers::Spi flashSpi {spi, Pinetime::PinMap::SpiFlashCsn};
Pinetime::Drivers::SpiNorFlash spiNorFlash {"spiNorFlash.raw"};

// The TWI device should work @ up to 400Khz but there is a HW bug which prevent it from
// respecting correct timings. According to erratas heet, this magic value makes it run
// at ~390Khz with correct timings.
static constexpr uint32_t MaxTwiFrequencyWithoutHardwareBug {0x06200000};
Pinetime::Drivers::TwiMaster twiMaster {NRF_TWIM1, MaxTwiFrequencyWithoutHardwareBug, Pinetime::PinMap::TwiSda, Pinetime::PinMap::TwiScl};
Pinetime::Drivers::Cst816S touchPanel; // {twiMaster, touchPanelTwiAddress};
//#ifdef PINETIME_IS_RECOVERY
//  #include "displayapp/DummyLittleVgl.h"
//  #include "displayapp/DisplayAppRecovery.h"
//#else
//  #include "displayapp/LittleVgl.h"
//  #include "displayapp/DisplayApp.h"
//#endif
Pinetime::Drivers::Bma421 motionSensor {twiMaster, motionSensorTwiAddress};
Pinetime::Drivers::Hrs3300 heartRateSensor {twiMaster, heartRateSensorTwiAddress};

TimerHandle_t debounceTimer;
TimerHandle_t debounceChargeTimer;
Pinetime::Controllers::Battery batteryController;
Pinetime::Controllers::Ble bleController;

Pinetime::Controllers::HeartRateController heartRateController;
Pinetime::Applications::HeartRateTask heartRateApp(heartRateSensor, heartRateController);

Pinetime::Controllers::FS fs {spiNorFlash};
Pinetime::Controllers::Settings settingsController {fs};
Pinetime::Controllers::MotorController motorController {};

Pinetime::Controllers::DateTime dateTimeController {settingsController};
Pinetime::Drivers::Watchdog watchdog;
Pinetime::Controllers::NotificationManager notificationManager;
Pinetime::Controllers::MotionController motionController;
#if defined(INFINITIME_TIMERCONTROLLER)
Pinetime::Controllers::TimerController timerController;
#endif

Pinetime::Controllers::AlarmController alarmController {dateTimeController};
Pinetime::Controllers::TouchHandler touchHandler;
Pinetime::Controllers::ButtonHandler buttonHandler;
Pinetime::Controllers::BrightnessController brightnessController {};

Pinetime::Applications::DisplayApp displayApp(lcd,
                                              touchPanel,
                                              batteryController,
                                              bleController,
                                              dateTimeController,
                                              watchdog,
                                              notificationManager,
                                              heartRateController,
                                              settingsController,
                                              motorController,
                                              motionController,
                                              #if defined(INFINITIME_TIMERCONTROLLER)
                                              timerController,
                                              #endif
                                              alarmController,
                                              brightnessController,
                                              touchHandler,
                                              fs);

Pinetime::System::SystemTask systemTask(spi,
                                        spiNorFlash,
                                        twiMaster,
                                        touchPanel,
                                        batteryController,
                                        bleController,
                                        dateTimeController,
                                        #if defined(INFINITIME_TIMERCONTROLLER)
                                        timerController,
                                        #endif
                                        alarmController,
                                        watchdog,
                                        notificationManager,
                                        heartRateSensor,
                                        motionController,
                                        motionSensor,
                                        settingsController,
                                        heartRateController,
                                        displayApp,
                                        heartRateApp,
                                        fs,
                                        touchHandler,
                                        buttonHandler);

// variable used in SystemTask.cpp Work loop
std::chrono::time_point<std::chrono::system_clock, std::chrono::nanoseconds> NoInit_BackUpTime;

class Framework {
public:
    // Contructor which initialize the parameters.
    Framework(bool visible_, int height_, int width_) :
      visible(visible_), height(height_), width(width_)
      {
        if (visible) {
          //SDL_Init(SDL_INIT_VIDEO);       // Initializing SDL as Video
          SDL_CreateWindowAndRenderer(width, height, 0, &window, &renderer);
          SDL_SetWindowTitle(window, "LV Simulator Status");
          { // move window a bit to the right, to not be over the PineTime Screen
              int x,y;
              SDL_GetWindowPosition(window, &x, &y);
              SDL_SetWindowPosition(window, x+LV_HOR_RES_MAX, y);
          }
          SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0);      // setting draw color
          SDL_RenderClear(renderer);      // Clear the newly created window
          SDL_RenderPresent(renderer);    // Reflects the changes done in the
                                          //  window.
        }
        init_NRF_WDT();
        init_NRF_POWER();

        // Attempt to load background BMP from memory for the status display window
        const size_t SIM_BACKGROUND_size = sizeof(SIM_BACKGROUND);
        SDL_RWops *rw = SDL_RWFromMem((void*)SIM_BACKGROUND, SIM_BACKGROUND_size);
        SDL_Surface* simDisplayBgRaw = SDL_LoadBMP_RW(rw, 1);
        if (simDisplayBgRaw == NULL) {
          printf("Failed to load sim background image: %s\n", SDL_GetError());
        } else {
          // convert the loaded image into a texture
          simDisplayTexture = SDL_CreateTextureFromSurface(renderer, simDisplayBgRaw);
          SDL_FreeSurface(simDisplayBgRaw);
          simDisplayBgRaw = NULL;
        }
        motorController.Init();
        settingsController.Init();

        printf("initial free_size = %u\n", xPortGetFreeHeapSize());

        // update time to current system time once on startup
        dateTimeController.SetCurrentTime(std::chrono::system_clock::now());

        systemTask.Start();

        // initialize the first LVGL screen
        //const auto clockface = settingsController.GetClockFace();
        //switch_to_screen(1+clockface);
    }

    // Destructor
    ~Framework(){
        if (simDisplayTexture != NULL) {
          SDL_DestroyTexture(simDisplayTexture);
        }
        SDL_DestroyRenderer(renderer);
        SDL_DestroyWindow(window);
        SDL_Quit();
    }

    void draw_circle_red(int center_x, int center_y, int radius_){
        SDL_SetRenderDrawColor(renderer, 255, 0, 0, 255);
        draw_circle_(center_x, center_y, radius_);
    }
    void draw_circle_green(int center_x, int center_y, int radius_){
        SDL_SetRenderDrawColor(renderer, 0, 255, 0, 255);
        draw_circle_(center_x, center_y, radius_);
    }
    void draw_circle_blue(int center_x, int center_y, int radius_){
        SDL_SetRenderDrawColor(renderer, 0, 0, 255, 255);
        draw_circle_(center_x, center_y, radius_);
    }
    void draw_circle_yellow(int center_x, int center_y, int radius_){
        SDL_SetRenderDrawColor(renderer, 255, 255, 0, 255);
        draw_circle_(center_x, center_y, radius_);
    }
    void draw_circle_grey(int center_x, int center_y, int radius_){
        SDL_SetRenderDrawColor(renderer, 128, 128, 128, 255);
        draw_circle_(center_x, center_y, radius_);
    }
    void draw_circle_white(int center_x, int center_y, int radius_){
        SDL_SetRenderDrawColor(renderer, 255, 255, 255, 255);
        draw_circle_(center_x, center_y, radius_);
    }
    void draw_circle_(int center_x, int center_y, int radius_){
        // Drawing circle
        for(int x=center_x-radius_; x<=center_x+radius_; x++){
            for(int y=center_y-radius_; y<=center_y+radius_; y++){
                if((std::pow(center_y-y,2)+std::pow(center_x-x,2)) <=
                    std::pow(radius_,2)){
                    SDL_RenderDrawPoint(renderer, x, y);
                }
            }
        }
    }

    void refresh() {
      // left edge for all "bubbles" (circles)
      constexpr const int bubbleLeftEdge = 65;
      // always refresh the LVGL screen
      this->refresh_screen();

      if (!visible) {
        return;
      }
        SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0);
        SDL_RenderClear(renderer);
        // Render the background if it was able to be loaded
        if (simDisplayTexture != NULL) {
          SDL_RenderCopy(renderer, simDisplayTexture, NULL, NULL);
        }
        { // motorController.motor_is_running
          constexpr const int center_x = bubbleLeftEdge;
          constexpr const int center_y = 216;
          bool motor_is_running = nrf_gpio_pin_read(Pinetime::PinMap::Motor);
          if (motor_is_running) {
              draw_circle_red(center_x, center_y, 15);
          } else {
              draw_circle_grey(center_x, center_y, 15);
          }
        }
        { // ble.motor_is_running
          constexpr const int center_x = bubbleLeftEdge;
          constexpr const int center_y = 24;
          if (bleController.IsConnected()) {
              draw_circle_blue(center_x, center_y, 15);
          } else {
              draw_circle_grey(center_x, center_y, 15);
          }
        }
        // batteryController.percentRemaining
        {
          const int center_x = bubbleLeftEdge;
          const int center_y = 164;
          const int max_bar_length = 150;
          const int filled_bar_length = max_bar_length * (batteryController.percentRemaining/100.0);
          const int rect_height = 14;
          SDL_Rect rect {
            .x = center_x - rect_height/2,
            .y = center_y,
            .w = max_bar_length,
            .h = rect_height
          };
          SDL_SetRenderDrawColor(renderer, 128, 128, 128, 255);
          SDL_RenderDrawRect(renderer, &rect);

          rect.w = filled_bar_length;
          rect.h++;
          rect.h-=2;
          SDL_SetRenderDrawColor(renderer, 0, 255, 0, 255);
          SDL_RenderFillRect(renderer, &rect);
          //set color and new x pos, draw again
        }
        { // batteryController.isCharging
          constexpr const int center_x = bubbleLeftEdge;
          constexpr const int center_y = 120;
          if (batteryController.isCharging) {
              draw_circle_yellow(center_x, center_y, 15);
          } else
          {
              draw_circle_grey(center_x, center_y, 15);
          }
        }
        { // brightnessController.Level
          constexpr const int center_y = 72;
          const Pinetime::Controllers::BrightnessController::Levels level = brightnessController.Level();
          uint8_t level_idx = 0;
          if (level == Pinetime::Controllers::BrightnessController::Levels::Low)
          {
            level_idx = 1;
          } else if (level == Pinetime::Controllers::BrightnessController::Levels::Medium)
          {
            level_idx = 2;
          } else if (level == Pinetime::Controllers::BrightnessController::Levels::High)
          {
            level_idx = 3;
          }
          for (uint8_t i=0; i<4; i++) {
            const int bubble_size = (i*2) + 5;
            const int center_x = bubbleLeftEdge + ((bubble_size+10) * i) - 5;
            if (i <= level_idx) {
              draw_circle_white(center_x, center_y, bubble_size);
            } else {
              draw_circle_grey(center_x, center_y, bubble_size);
            }
          }
        }
        // Show the change on the screen
        SDL_RenderPresent(renderer);
    }

    // prepared notficitions, one per message category
    const std::vector<std::string> notification_messages {
      "0category:\nUnknown",
      "Lorem ipsum\ndolor sit amet,\nconsectetur adipiscing elit,\n",
      "1SimpleAlert",
      "sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
      "2Email:",
      "Vitae aliquet nec ullamcorper sit amet.",
      "3News:",
      "Id\naliquet\nrisus\nfeugiat\nin\nante\nmetus\ndictum\nat.",
      "4IncomingCall:",
      "Ut porttitor leo a diam sollicitudin.",
      "5MissedCall:",
      "Ultrices tincidunt arcu non sodales neque sodales ut etiam sit.",
      "6Sms:",
      "Pellentesque dignissim enim sit amet.",
      "7VoiceMail:",
      "Urna nec tincidunt praesent semper feugiat nibh sed pulvinar proin.",
      "8Schedule:",
      "Tellus id interdum velit laoreet id donec ultrices tincidunt.",
      "9HighProriotyAlert:",
      "Viverra maecenas accumsan lacus vel facilisis volutpat est velit egestas.",
      "10InstantMessage:",
      "Volutpat consequat mauris nunc congue.",
    };
    size_t notification_idx = 0; // which message to send next
    void send_notification() {
      Pinetime::Controllers::NotificationManager::Notification notif;
      const std::string &title   = notification_messages.at(notification_idx*2);
      const std::string &message = notification_messages.at(notification_idx*2+1);
      std::copy(title.begin(), title.end(), notif.message.data());
      notif.message[title.size()] = '\0'; // title and message is \0 separated
      std::copy(message.begin(), message.end(), notif.message.data()+title.size()+1);
      notif.message[title.size() + 1 + message.size()] = '\0'; // zero terminate the message
      notif.size = title.size() + 1 + message.size();
      notif.category = static_cast<Pinetime::Controllers::NotificationManager::Categories>(notification_idx % 11);
      notificationManager.Push(std::move(notif));
      // send next message the next time
      notification_idx++;
      if (notification_idx >= notification_messages.size()/2) {
        notification_idx = 0;
      }
      if (settingsController.GetNotificationStatus() == Pinetime::Controllers::Settings::Notification::On)
      {
        if (screen_off_created) {
          // wake up! (deletes screen_off label)
          systemTask.PushMessage(Pinetime::System::Messages::GoToRunning);
        }
        displayApp.PushMessage(Pinetime::Applications::Display::Messages::NewNotification);
      }
    }

    // can't use SDL_PollEvent, as those are fed to lvgl
    // implement a non-descructive key-pressed handler (as not consuming SDL_Events)
    void handle_keys() {
      const Uint8 *state = SDL_GetKeyboardState(NULL);
      const bool key_shift = state[SDL_SCANCODE_LSHIFT] || state[SDL_SCANCODE_RSHIFT];
      auto debounce = [&] (const char key_low, const char key_capital, const bool scancode, bool &key_handled) {
        if (scancode && !key_handled) {
          if (key_shift) {
            this->handle_key(key_capital);
          } else {
            this->handle_key(key_low);
          }
          key_handled = true;
        } else if (scancode && key_handled) {
          // ignore, already handled
        } else {
          key_handled = false;
        }
      };
      debounce('r', 'R', state[SDL_SCANCODE_R], key_handled_r);
      debounce('n', 'N', state[SDL_SCANCODE_N], key_handled_n);
      debounce('m', 'M', state[SDL_SCANCODE_M], key_handled_m);
      debounce('b', 'B', state[SDL_SCANCODE_B], key_handled_b);
      debounce('v', 'V', state[SDL_SCANCODE_V], key_handled_v);
      debounce('c', 'C', state[SDL_SCANCODE_C], key_handled_c);
      debounce('l', 'L', state[SDL_SCANCODE_L], key_handled_l);
      debounce('p', 'P', state[SDL_SCANCODE_P], key_handled_p);
      debounce('s', 'S', state[SDL_SCANCODE_S], key_handled_s);
      debounce('h', 'H', state[SDL_SCANCODE_H], key_handled_h);
      debounce('i', 'I', state[SDL_SCANCODE_I], key_handled_i);
      // screen switcher buttons
      debounce('1', '!'+1, state[SDL_SCANCODE_1], key_handled_1);
      debounce('2', '!'+2, state[SDL_SCANCODE_2], key_handled_2);
      debounce('3', '!'+3, state[SDL_SCANCODE_3], key_handled_3);
      debounce('4', '!'+4, state[SDL_SCANCODE_4], key_handled_4);
      debounce('5', '!'+5, state[SDL_SCANCODE_5], key_handled_5);
      debounce('6', '!'+6, state[SDL_SCANCODE_6], key_handled_6);
      debounce('7', '!'+7, state[SDL_SCANCODE_7], key_handled_7);
      debounce('8', '!'+8, state[SDL_SCANCODE_8], key_handled_8);
      debounce('9', '!'+9, state[SDL_SCANCODE_9], key_handled_9);
      debounce('0', '!'+0, state[SDL_SCANCODE_0], key_handled_0);
      // direction keys
      debounce(':', ':', state[SDL_SCANCODE_UP], key_handled_up);
      debounce(';', ';', state[SDL_SCANCODE_DOWN], key_handled_down);
      debounce('<', '<', state[SDL_SCANCODE_LEFT], key_handled_left);
      debounce('>', '>', state[SDL_SCANCODE_RIGHT], key_handled_right);
    }
    // inject a swipe gesture to the touch handler and notify displayapp to notice it
    void send_gesture(Pinetime::Drivers::Cst816S::Gestures gesture)
    {
      Pinetime::Drivers::Cst816S::TouchInfos info;
      info.isValid = true;
      info.touching = true;
      info.gesture = gesture;
      touchHandler.ProcessTouchInfo(info);
      displayApp.PushMessage(Pinetime::Applications::Display::Messages::TouchEvent);
      info.touching = false;
      info.gesture = Pinetime::Drivers::Cst816S::Gestures::None;
      touchHandler.ProcessTouchInfo(info);
    }
    // modify the simulated controller depending on the pressed key
    void handle_key(SDL_Keycode key) {
      if (key == 'r') {
        motorController.StartRinging();
      } else if (key == 'R') {
        motorController.StopRinging();
      } else if (key == 'm') {
        motorController.RunForDuration(100);
      } else if (key == 'M') {
        motorController.RunForDuration(255);
      } else if (key == 'n') {
        send_notification();
      } else if (key == 'N') {
        notificationManager.ClearNewNotificationFlag();
      } else if (key == 'b') {
        bleController.Connect();
      } else if (key == 'B') {
        bleController.Disconnect();
      } else if (key == 'v') {
        if (batteryController.percentRemaining >= 90) {
          batteryController.percentRemaining = 100;
        } else {
          batteryController.percentRemaining += 10;
        }
      } else if (key == 'V') {
        if (batteryController.percentRemaining <= 10) {
          batteryController.percentRemaining = 0;
        } else {
          batteryController.percentRemaining -= 10;
        }
      } else if (key == 'c') {
        batteryController.isCharging = true;
        batteryController.isPowerPresent = true;
      } else if (key == 'C') {
        batteryController.isCharging = false;
        batteryController.isPowerPresent = false;
      } else if (key == 'l' && !screen_off_created) {
        brightnessController.Higher();
      } else if (key == 'L' && !screen_off_created) {
        brightnessController.Lower();
      } else if (key == 'p') {
        this->print_memory_usage = true;
      } else if (key == 'P') {
        this->print_memory_usage = false;
      } else if (key == 's') {
        motionSensor.steps += 500;
      } else if (key == 'S') {
        if (motionSensor.steps > 500) {
          motionSensor.steps -= 500;
        } else {
          motionSensor.steps = 0;
        }
      } else if (key == 'h') {
        if (heartRateController.State() == Pinetime::Controllers::HeartRateController::States::Stopped) {
          heartRateController.Start();
        } else if (heartRateController.State() == Pinetime::Controllers::HeartRateController::States::NotEnoughData) {
          heartRateController.Update(Pinetime::Controllers::HeartRateController::States::Running, 10);
        } else {
          uint8_t heartrate = heartRateController.HeartRate();
          heartRateController.Update(Pinetime::Controllers::HeartRateController::States::Running, heartrate + 10);
        }
      } else if (key == 'H') {
        heartRateController.Stop();
      } else if (key == 'i') {
        saveScreenshot();
      } else if (key == 'I') {
        if (!gif_manager.is_in_progress())
        {
          gif_manager.create_new();
        } else {
          gif_manager.close();
        }
      } else if (key >= '0' && key <= '9') {
        this->switch_to_screen(key-'0');
      } else if (key >= '!'+0 && key <= '!'+9) {
        this->switch_to_screen(key-'!'+10);
      } else if (key == ':') { // up
        send_gesture(Pinetime::Drivers::Cst816S::Gestures::SlideUp);
      } else if (key == ';') { // down
        send_gesture(Pinetime::Drivers::Cst816S::Gestures::SlideDown);
      } else if (key == '<') { // left
        send_gesture(Pinetime::Drivers::Cst816S::Gestures::SlideLeft);
      } else if (key == '>') { // up
        send_gesture(Pinetime::Drivers::Cst816S::Gestures::SlideRight);
      }
      batteryController.voltage = batteryController.percentRemaining * 50;
    }

    void handle_touch_and_button() {
      int x, y;
      uint32_t buttons = SDL_GetMouseState(&x, &y);
      const bool left_click = (buttons & SDL_BUTTON_LMASK) != 0;
      const bool right_click = (buttons & SDL_BUTTON_RMASK) != 0;
      if (left_click) {
        left_release_sent = false;
        systemTask.OnTouchEvent();
        return;
      } else {
        if (!left_release_sent) {
          left_release_sent = true;
          systemTask.OnTouchEvent();
          return;
        }
      }
      if (right_click != right_last_state) {
        right_last_state =right_click;
        systemTask.PushMessage(Pinetime::System::Messages::HandleButtonEvent);
        return;
      }
    }

    // helper function to switch between screens
    void switch_to_screen(uint8_t screen_idx)
    {
      if (screen_idx == 1) {
        settingsController.SetWatchFace(Pinetime::Applications::WatchFace::Digital);
        displayApp.StartApp(Pinetime::Applications::Apps::Clock, Pinetime::Applications::DisplayApp::FullRefreshDirections::None);
      }
      else if (screen_idx == 2) {
        settingsController.SetWatchFace(Pinetime::Applications::WatchFace::Analog);
        displayApp.StartApp(Pinetime::Applications::Apps::Clock, Pinetime::Applications::DisplayApp::FullRefreshDirections::None);
      }
      else if (screen_idx == 3) {
        settingsController.SetWatchFace(Pinetime::Applications::WatchFace::PineTimeStyle);
        displayApp.StartApp(Pinetime::Applications::Apps::Clock, Pinetime::Applications::DisplayApp::FullRefreshDirections::None);
      }
      else if (screen_idx == 4) {
        displayApp.StartApp(Pinetime::Applications::Apps::Paddle, Pinetime::Applications::DisplayApp::FullRefreshDirections::None);
      }
      else if (screen_idx == 5) {
        displayApp.StartApp(Pinetime::Applications::Apps::Twos, Pinetime::Applications::DisplayApp::FullRefreshDirections::None);
      }
      else if (screen_idx == 6) {
        displayApp.StartApp(Pinetime::Applications::Apps::Metronome, Pinetime::Applications::DisplayApp::FullRefreshDirections::None);
      }
      else if (screen_idx == 7) {
        displayApp.StartApp(Pinetime::Applications::Apps::FirmwareUpdate, Pinetime::Applications::DisplayApp::FullRefreshDirections::None);
      }
      else if (screen_idx == 8) {
        displayApp.StartApp(Pinetime::Applications::Apps::BatteryInfo, Pinetime::Applications::DisplayApp::FullRefreshDirections::None);
      }
      else if (screen_idx == 9) {
        displayApp.StartApp(Pinetime::Applications::Apps::FlashLight, Pinetime::Applications::DisplayApp::FullRefreshDirections::None);
      }
      else if (screen_idx == 0) {
        displayApp.StartApp(Pinetime::Applications::Apps::QuickSettings, Pinetime::Applications::DisplayApp::FullRefreshDirections::None);
      }
      else if (screen_idx == 11) {
        displayApp.StartApp(Pinetime::Applications::Apps::Music, Pinetime::Applications::DisplayApp::FullRefreshDirections::None);
      }
      else if (screen_idx == 12) {
        displayApp.StartApp(Pinetime::Applications::Apps::Paint, Pinetime::Applications::DisplayApp::FullRefreshDirections::None);
      }
      else if (screen_idx == 13) {
        displayApp.StartApp(Pinetime::Applications::Apps::SysInfo, Pinetime::Applications::DisplayApp::FullRefreshDirections::None);
      }
      else if (screen_idx == 14) {
        displayApp.StartApp(Pinetime::Applications::Apps::Steps, Pinetime::Applications::DisplayApp::FullRefreshDirections::None);
      }
      else if (screen_idx == 15) {
        displayApp.StartApp(Pinetime::Applications::Apps::Error, Pinetime::Applications::DisplayApp::FullRefreshDirections::None);
      }
      else if (screen_idx == 17) {
        displayApp.StartApp(Pinetime::Applications::Apps::PassKey, Pinetime::Applications::DisplayApp::FullRefreshDirections::None);
      }
      else {
        std::cout << "unhandled screen_idx: " << int(screen_idx) << std::endl;
      }
    }
    static void screen_off_delete_cb(lv_obj_t *obj, lv_event_t event)
    {
      if (event == LV_EVENT_DELETE) {
        auto* fw = static_cast<Framework*>(obj->user_data);
        if (obj == fw->screen_off_bg)
        {
          // on delete make sure to not double free the screen_off objects
          fw->screen_off_created = false;
        }
      }
    }
    // render the current status of the simulated controller
    void refresh_screen() {
      const Pinetime::Controllers::BrightnessController::Levels level = brightnessController.Level();
      if (level == Pinetime::Controllers::BrightnessController::Levels::Off) {
        if (!screen_off_created) {
          screen_off_created = true;
          screen_off_bg = lv_obj_create(lv_scr_act(), nullptr);
          lv_obj_set_size(screen_off_bg, 240, 240);
          lv_obj_set_pos(screen_off_bg, 0, 0);
          lv_obj_set_style_local_bg_color(screen_off_bg, LV_OBJ_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_BLACK);
          screen_off_bg->user_data = this; // add callback to prevent double free through Alarm Screen
          lv_obj_set_event_cb(screen_off_bg, screen_off_delete_cb);
          screen_off_label = lv_label_create(lv_scr_act(), nullptr);
          lv_label_set_text_static(screen_off_label, "Screen is OFF");
          lv_obj_align(screen_off_label, nullptr, LV_ALIGN_CENTER, 0, -20);
        }
        /* Periodically call the lv_task handler.
        * It could be done in a timer interrupt or an OS task too.*/
        // only call the task handler if the screen is off,
        // when the screen is enabled the call is done in the SystemTask class
        lv_task_handler();
      } else {
        if (screen_off_created) {
          screen_off_created = false;
          lv_obj_del(screen_off_bg);
          lv_obj_del(screen_off_label);
        }
      }

      if (print_memory_usage) {
        auto currentFreeHeap = xPortGetFreeHeapSize();
        if (currentFreeHeap != lastFreeHeapSize) {
          auto minimumEverFreeHeap = xPortGetMinimumEverFreeHeapSize();
          // 14KiB is the LVGL memory size used in InfiniTime
          constexpr uint32_t pinetime_heap_memory = configTOTAL_HEAP_SIZE;
          uint32_t mem_used = pinetime_heap_memory - currentFreeHeap;
          // The "budget" value shows how much free lvgl memory the PineTime
          // would have free and will go negative when more memory is used
          // in the simulator than is available on the real hardware.
          int32_t budget = configTOTAL_HEAP_SIZE - mem_used;
          printf("Mem: %5u used (change: %+5d, peak: %5u) %d budget left\n", mem_used, lastFreeHeapSize - currentFreeHeap, minimumEverFreeHeap, budget);
          lastFreeHeapSize = currentFreeHeap;
        }
      }

      if (gif_manager.is_in_progress())
      {
        gif_manager.write_frame();
      }
    }

    bool print_memory_usage = false;

    // variables to create and destroy an lvgl overlay to indicate a turned off screen
    bool screen_off_created = false;
    lv_obj_t *screen_off_bg;
    lv_obj_t *screen_off_label;

private:
    bool key_handled_r = false; // r ... enable ringing, R ... disable ringing
    bool key_handled_m = false; // m ... let motor run, M ... stop motor
    bool key_handled_n = false; // n ... send notification, N ... clear new notification flag
    bool key_handled_b = false; // b ... connect Bluetooth, B ... disconnect Bluetooth
    bool key_handled_v = false; // battery Voltage and percentage, v ... increase, V ... decrease
    bool key_handled_c = false; // c ... charging, C ... not charging
    bool key_handled_l = false; // l ... increase brightness level, L ... lower brightness level
    bool key_handled_p = false; // p ... enable print memory usage, P ... disable print memory usage
    bool key_handled_s = false; // s ... increase step count, S ... decrease step count
    bool key_handled_h = false; // h ... set heartrate running, H ... stop heartrate
    bool key_handled_i = false; // i ... take screenshot, I ... start/stop Gif screen capture
    // numbers from 0 to 9 to switch between screens
    bool key_handled_1 = false;
    bool key_handled_2 = false;
    bool key_handled_3 = false;
    bool key_handled_4 = false;
    bool key_handled_5 = false;
    bool key_handled_6 = false;
    bool key_handled_7 = false;
    bool key_handled_8 = false;
    bool key_handled_9 = false;
    bool key_handled_0 = false;
    // direction arrows
    bool key_handled_up = false;     // inject swipe up
    bool key_handled_down = false;   // inject swipe down
    bool key_handled_left = false;   // inject swipe left
    bool key_handled_right = false;  // inject swipe right
    bool visible;   // show Simulator window
    int height;     // Height of the window
    int width;      // Width of the window
    SDL_Renderer *renderer = NULL;      // Pointer for the renderer
    SDL_Window *window = NULL;      // Pointer for the window
    SDL_Texture* simDisplayTexture = NULL; // Background for the sim status window

    bool left_release_sent = true; // make sure to send one mouse button release event
    bool right_last_state = false; // varable used to send message only on changing state

    size_t lastFreeHeapSize = configTOTAL_HEAP_SIZE;

    GifManager gif_manager;
};

int mallocFailedCount = 0;
int stackOverflowCount = 0;
int main(int argc, char **argv)
{
  // parse arguments
  bool fw_status_window_visible = true;
  bool arg_help = false;
  for (int i=1; i<argc; i++)
  {
    const std::string arg(argv[i]);
    if (arg == "--hide-status")
    {
      fw_status_window_visible = false;
    } else if (arg == "-h" || arg == "--help")
    {
      arg_help = true;
    } else
    {
      std::cout << "unknown argument '" << arg << "'" << std::endl;
      return 1;
    }
  }
  if (arg_help) {
    std::cout << "Usage: " << argv[0] << " [options]" << std::endl;
    std::cout << "Options:" << std::endl;
    std::cout << "  -h, --help           show this help message and exit" << std::endl;
    std::cout << "      --hide-status    don't show simulator status window, so only lvgl window is open" << std::endl;
    return 0;
  }

  /*Initialize LVGL*/
  lv_init();

  /*Initialize the HAL (display, input devices, tick) for LVGL*/
  hal_init();

  fs.Init();

  // initialize the core of our Simulator
  Framework fw(fw_status_window_visible, 240,240);

  while(1) {
      fw.handle_keys(); // key event polling
      fw.handle_touch_and_button();
      fw.refresh();
      usleep(LV_DISP_DEF_REFR_PERIOD * 1000);
  }

  return 0;
}

/**********************
 *   STATIC FUNCTIONS
 **********************/

/**
 * Initialize the Hardware Abstraction Layer (HAL) for the LVGL graphics
 * library
 */
static void hal_init(void)
{
  /* Use the 'monitor' driver which creates window on PC's monitor to simulate a display*/
  monitor_init();
  /* Tick init.
   * You have to call 'lv_tick_inc()' in periodically to inform LittelvGL about
   * how much time were elapsed Create an SDL thread to do this*/
  SDL_CreateThread(tick_thread, "tick", NULL);

  // use pinetime_theme
  //lv_theme_t* th = lv_pinetime_theme_init(
  //  LV_COLOR_WHITE, LV_COLOR_SILVER, 0, &jetbrains_mono_bold_20, &jetbrains_mono_bold_20, &jetbrains_mono_bold_20, &jetbrains_mono_bold_20);
  //lv_theme_set_act(th);

  ///*Create a display buffer*/
  //static lv_disp_buf_t disp_buf1;
  //static lv_color_t buf1_1[LV_HOR_RES_MAX * 120];
  //lv_disp_buf_init(&disp_buf1, buf1_1, NULL, LV_HOR_RES_MAX * 120);

  ///*Create a display*/
  //lv_disp_drv_t disp_drv;
  //lv_disp_drv_init(&disp_drv); /*Basic initialization*/
  //disp_drv.buffer = &disp_buf1;
  //disp_drv.flush_cb = monitor_flush;
  //lv_disp_drv_register(&disp_drv);

  /* Add the mouse as input device
   * Use the 'mouse' driver which reads the PC's mouse*/
  //mouse_init();
  //static lv_indev_drv_t indev_drv_1;
  //lv_indev_drv_init(&indev_drv_1); /*Basic initialization*/
  //indev_drv_1.type = LV_INDEV_TYPE_POINTER;

  /*This function will be called periodically (by the library) to get the mouse position and state*/
  //indev_drv_1.read_cb = mouse_read;
  //mouse_indev = lv_indev_drv_register(&indev_drv_1);

  /*Add the keyboard as input device.*/
  //lv_indev_drv_t kb_drv;
  //lv_indev_drv_init(&kb_drv);
  //kb_drv.type = LV_INDEV_TYPE_KEYPAD;
  //kb_drv.read_cb = keyboard_read;
  //lv_indev_drv_register(&kb_drv);

  /*Add the mousewheel as input device.*/
  //lv_indev_drv_t enc_drv;
  //lv_indev_drv_init(&enc_drv);
  //enc_drv.type = LV_INDEV_TYPE_ENCODER;
  //enc_drv.read_cb = mousewheel_read;
  //lv_indev_drv_register(&enc_drv);

  /*Set a cursor for the mouse*/
  //LV_IMG_DECLARE(mouse_cursor_icon); /*Declare the image file.*/
  //lv_obj_t * cursor_obj = lv_img_create(lv_scr_act(), NULL); /*Create an image object for the cursor */
  //lv_img_set_src(cursor_obj, &mouse_cursor_icon);           /*Set the image source*/
  //lv_indev_set_cursor(mouse_indev, cursor_obj);             /*Connect the image  object to the driver*/
}

/**
 * A task to measure the elapsed time for LVGL
 * @param data unused
 * @return never return
 */
static int tick_thread(void *data) {
    (void)data;

    while(1) {
        SDL_Delay(LV_DISP_DEF_REFR_PERIOD);
        lv_tick_inc(LV_DISP_DEF_REFR_PERIOD); /*Tell LittelvGL that 30 milliseconds were elapsed*/
    }

    return 0;
}