/**
 * Marlin 3D Printer Firmware
 * Copyright (c) 2021 MarlinFirmware [https://github.com/MarlinFirmware/Marlin]
 *
 * Based on Sprinter and grbl.
 * Copyright (c) 2011 Camiel Gubbels / Erik van der Zalm
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 */

#include "../../../inc/MarlinConfigPre.h"

#if ENABLED(DGUS_LCD_UI_RELOADED)

#include "DGUSScreenHandler.h"

#include "DGUSDisplay.h"
#include "definition/DGUS_ScreenAddrList.h"
#include "definition/DGUS_ScreenSetup.h"

#include "../../../gcode/queue.h"

uint8_t DGUSScreenHandler::debug_count = 0;

#if ENABLED(SDSUPPORT)
  ExtUI::FileList DGUSScreenHandler::filelist;
  uint16_t DGUSScreenHandler::filelist_offset = 0;
  int16_t DGUSScreenHandler::filelist_selected = -1;
#endif

DGUS_Data::StepSize DGUSScreenHandler::offset_steps = DGUS_Data::StepSize::MMP1;
DGUS_Data::StepSize DGUSScreenHandler::move_steps = DGUS_Data::StepSize::MM10;

uint16_t DGUSScreenHandler::probing_icons[] = { 0, 0 };

DGUS_Data::Extruder DGUSScreenHandler::filament_extruder = DGUS_Data::Extruder::CURRENT;
uint16_t DGUSScreenHandler::filament_length = DGUS_DEFAULT_FILAMENT_LEN;

char DGUSScreenHandler::gcode[] = "";

DGUS_Data::Heater DGUSScreenHandler::pid_heater = DGUS_Data::Heater::H0;
uint16_t DGUSScreenHandler::pid_temp = DGUS_PLA_TEMP_HOTEND;
uint8_t DGUSScreenHandler::pid_cycles = 5;

bool DGUSScreenHandler::settings_ready = false;
bool DGUSScreenHandler::booted = false;

DGUS_Screen DGUSScreenHandler::current_screen = DGUS_Screen::BOOT;
DGUS_Screen DGUSScreenHandler::new_screen = DGUS_Screen::BOOT;
bool DGUSScreenHandler::full_update = false;

DGUS_Screen DGUSScreenHandler::wait_return_screen = DGUS_Screen::HOME;
bool DGUSScreenHandler::wait_continue = false;

bool DGUSScreenHandler::leveling_active = false;

millis_t DGUSScreenHandler::status_expire = 0;
millis_t DGUSScreenHandler::eeprom_save = 0;

const char DGUS_MSG_HOMING_REQUIRED[] PROGMEM = "Homing required",
           DGUS_MSG_BUSY[] PROGMEM = "Busy",
           DGUS_MSG_UNDEF[] PROGMEM = "-",
           DGUS_MSG_HOMING[] PROGMEM = "Homing...",
           DGUS_MSG_FW_OUTDATED[] PROGMEM = "DWIN GUI/OS update required",
           DGUS_MSG_ABL_REQUIRED[] PROGMEM = "Auto bed leveling required";

const char DGUS_CMD_HOME[] PROGMEM = "G28",
           DGUS_CMD_EEPROM_SAVE[] PROGMEM = "M500";

void DGUSScreenHandler::Init() {
  dgus_display.Init();

  MoveToScreen(DGUS_Screen::BOOT, true);
}

void DGUSScreenHandler::Ready() {
  dgus_display.PlaySound(1);
}

void DGUSScreenHandler::Loop() {
  if (!settings_ready || current_screen == DGUS_Screen::KILL) {
    return;
  }

  const millis_t ms = ExtUI::safe_millis();
  static millis_t next_event_ms = 0;

  if (new_screen != DGUS_Screen::BOOT) {
    const DGUS_Screen screen = new_screen;
    new_screen = DGUS_Screen::BOOT;
    if (current_screen == screen)
      TriggerFullUpdate();
    else
      MoveToScreen(screen);
    return;
  }

  if (!booted && ELAPSED(ms, 3000)) {
    booted = true;

    dgus_display.ReadVersions();

    if (current_screen == DGUS_Screen::BOOT)
      MoveToScreen(DGUS_Screen::HOME);

    return;
  }

  if (ELAPSED(ms, next_event_ms) || full_update) {
    next_event_ms = ms + DGUS_UPDATE_INTERVAL_MS;

    if (!SendScreenVPData(current_screen, full_update))
      DEBUG_ECHOLNPGM("SendScreenVPData failed");

    return;
  }

  if (current_screen == DGUS_Screen::WAIT
      && ((wait_continue && !wait_for_user)
          || (!wait_continue && IsPrinterIdle()))
  ) {
    MoveToScreen(wait_return_screen, true);
    return;
  }

  if (current_screen == DGUS_Screen::LEVELING_PROBING && IsPrinterIdle()) {
    dgus_display.PlaySound(3);

    SetStatusMessage(ExtUI::getMeshValid() ? F("Probing successful") : F("Probing failed"));

    MoveToScreen(DGUS_Screen::LEVELING_AUTOMATIC);
    return;
  }

  if (status_expire > 0 && ELAPSED(ms, status_expire)) {
    SetStatusMessage(FPSTR(NUL_STR), 0);
    return;
  }

  if (eeprom_save > 0 && ELAPSED(ms, eeprom_save) && IsPrinterIdle()) {
    eeprom_save = 0;
    queue.enqueue_now_P(DGUS_CMD_EEPROM_SAVE);
    return;
  }

  dgus_display.Loop();
}

void DGUSScreenHandler::PrinterKilled(FSTR_P const error, FSTR_P const component) {
  SetMessageLinePGM(FTOP(error), 1);
  SetMessageLinePGM(FTOP(component), 2);
  SetMessageLinePGM(NUL_STR, 3);
  SetMessageLinePGM(GET_TEXT(MSG_PLEASE_RESET), 4);

  dgus_display.PlaySound(3, 1, 200);

  MoveToScreen(DGUS_Screen::KILL, true);
}

void DGUSScreenHandler::UserConfirmRequired(const char * const msg) {
  dgus_screen_handler.SetMessageLinePGM(NUL_STR, 1);
  dgus_screen_handler.SetMessageLine(msg, 2);
  dgus_screen_handler.SetMessageLinePGM(NUL_STR, 3);
  dgus_screen_handler.SetMessageLinePGM(NUL_STR, 4);

  dgus_display.PlaySound(3);

  dgus_screen_handler.ShowWaitScreen(current_screen, true);
}

void DGUSScreenHandler::SettingsReset() {
  dgus_display.SetVolume(DGUS_DEFAULT_VOLUME);
  dgus_display.SetBrightness(DGUS_DEFAULT_BRIGHTNESS);

  if (!settings_ready) {
    settings_ready = true;
    Ready();
  }

  SetStatusMessage(F("EEPROM reset"));
}

void DGUSScreenHandler::StoreSettings(char *buff) {
  eeprom_data_t data;

  static_assert(sizeof(data) <= ExtUI::eeprom_data_size, "sizeof(eeprom_data_t) > eeprom_data_size.");

  data.initialized = true;
  data.volume = dgus_display.GetVolume();
  data.brightness = dgus_display.GetBrightness();
  data.abl_okay = (ExtUI::getLevelingActive() && ExtUI::getMeshValid());

  memcpy(buff, &data, sizeof(data));
}

void DGUSScreenHandler::LoadSettings(const char *buff) {
  eeprom_data_t data;

  static_assert(sizeof(data) <= ExtUI::eeprom_data_size, "sizeof(eeprom_data_t) > eeprom_data_size.");

  memcpy(&data, buff, sizeof(data));

  dgus_display.SetVolume(data.initialized ? data.volume : DGUS_DEFAULT_VOLUME);
  dgus_display.SetBrightness(data.initialized ? data.brightness : DGUS_DEFAULT_BRIGHTNESS);

  if (data.initialized) {
    leveling_active = (data.abl_okay && ExtUI::getMeshValid());
    ExtUI::setLevelingActive(leveling_active);
  }
}

void DGUSScreenHandler::ConfigurationStoreWritten(bool success) {
  if (!success)
    SetStatusMessage(F("EEPROM write failed"));
}

void DGUSScreenHandler::ConfigurationStoreRead(bool success) {
  if (!success) {
    SetStatusMessage(F("EEPROM read failed"));
  }
  else if (!settings_ready) {
    settings_ready = true;
    Ready();
  }
}

void DGUSScreenHandler::PlayTone(const uint16_t frequency, const uint16_t duration) {
  UNUSED(duration);

  if (frequency >= 1 && frequency <= 255) {
    if (duration >= 1 && duration <= 255)
      dgus_display.PlaySound((uint8_t)frequency, (uint8_t)duration);
    else
      dgus_display.PlaySound((uint8_t)frequency);
  }
}

void DGUSScreenHandler::MeshUpdate(const int8_t xpos, const int8_t ypos) {
  if (current_screen != DGUS_Screen::LEVELING_PROBING) {
    if (current_screen == DGUS_Screen::LEVELING_AUTOMATIC)
      TriggerFullUpdate();
    return;
  }

  uint8_t point = ypos * GRID_MAX_POINTS_X + xpos;
  probing_icons[point < 16 ? 0 : 1] |= (1U << (point % 16));

  if (xpos >= GRID_MAX_POINTS_X - 1 && ypos >= GRID_MAX_POINTS_Y - 1 && !ExtUI::getMeshValid())
    probing_icons[0] = probing_icons[1] = 0;

  TriggerFullUpdate();
}

void DGUSScreenHandler::PrintTimerStarted() {
  TriggerScreenChange(DGUS_Screen::PRINT_STATUS);
}

void DGUSScreenHandler::PrintTimerPaused() {
  dgus_display.PlaySound(3);
  TriggerFullUpdate();
}

void DGUSScreenHandler::PrintTimerStopped() {
  if (current_screen != DGUS_Screen::PRINT_STATUS && current_screen != DGUS_Screen::PRINT_ADJUST)
    return;

  dgus_display.PlaySound(3);

  TriggerScreenChange(DGUS_Screen::PRINT_FINISHED);
}

void DGUSScreenHandler::FilamentRunout(const ExtUI::extruder_t extruder) {
  char buffer[21];
  snprintf_P(buffer, sizeof(buffer), PSTR("Filament runout E%d"), extruder);

  SetStatusMessage(buffer);

  dgus_display.PlaySound(3);
}

#if ENABLED(SDSUPPORT)

  void DGUSScreenHandler::SDCardInserted() {
    if (current_screen == DGUS_Screen::HOME)
      TriggerScreenChange(DGUS_Screen::PRINT);
  }

  void DGUSScreenHandler::SDCardRemoved() {
    if (current_screen == DGUS_Screen::PRINT)
      TriggerScreenChange(DGUS_Screen::HOME);
  }

  void DGUSScreenHandler::SDCardError() {
    SetStatusMessage(GET_TEXT_F(MSG_MEDIA_READ_ERROR));
    if (current_screen == DGUS_Screen::PRINT)
      TriggerScreenChange(DGUS_Screen::HOME);
  }

#endif // SDSUPPORT

#if ENABLED(POWER_LOSS_RECOVERY)

  void DGUSScreenHandler::PowerLossResume() {
    MoveToScreen(DGUS_Screen::POWERLOSS, true);
  }

#endif // POWER_LOSS_RECOVERY

#if HAS_PID_HEATING

  void DGUSScreenHandler::PidTuning(const ExtUI::result_t rst) {
    switch (rst) {
      case ExtUI::PID_STARTED:
        SetStatusMessage(GET_TEXT_F(MSG_PID_AUTOTUNE));
        break;
      case ExtUI::PID_BAD_EXTRUDER_NUM:
        SetStatusMessage(GET_TEXT_F(MSG_PID_BAD_EXTRUDER_NUM));
        break;
      case ExtUI::PID_TEMP_TOO_HIGH:
        SetStatusMessage(GET_TEXT_F(MSG_PID_TEMP_TOO_HIGH));
        break;
      case ExtUI::PID_TUNING_TIMEOUT:
        SetStatusMessage(GET_TEXT_F(MSG_PID_TIMEOUT));
        break;
      case ExtUI::PID_DONE:
        SetStatusMessage(GET_TEXT_F(MSG_PID_AUTOTUNE_DONE));
        break;
      default:
        return;
    }

    dgus_display.PlaySound(3);
  }

#endif // HAS_PID_HEATING

void DGUSScreenHandler::SetMessageLine(const char* msg, uint8_t line) {
  switch (line) {
    default: return;
    case 1:
      dgus_display.WriteString((uint16_t)DGUS_Addr::MESSAGE_Line1, msg, DGUS_LINE_LEN, true, true);
      break;
    case 2:
      dgus_display.WriteString((uint16_t)DGUS_Addr::MESSAGE_Line2, msg, DGUS_LINE_LEN, true, true);
      break;
    case 3:
      dgus_display.WriteString((uint16_t)DGUS_Addr::MESSAGE_Line3, msg, DGUS_LINE_LEN, true, true);
      break;
    case 4:
      dgus_display.WriteString((uint16_t)DGUS_Addr::MESSAGE_Line4, msg, DGUS_LINE_LEN, true, true);
      break;
  }
}

void DGUSScreenHandler::SetMessageLinePGM(PGM_P msg, uint8_t line) {
  switch (line) {
    default: return;
    case 1:
      dgus_display.WriteStringPGM((uint16_t)DGUS_Addr::MESSAGE_Line1, msg, DGUS_LINE_LEN, true, true);
      break;
    case 2:
      dgus_display.WriteStringPGM((uint16_t)DGUS_Addr::MESSAGE_Line2, msg, DGUS_LINE_LEN, true, true);
      break;
    case 3:
      dgus_display.WriteStringPGM((uint16_t)DGUS_Addr::MESSAGE_Line3, msg, DGUS_LINE_LEN, true, true);
      break;
    case 4:
      dgus_display.WriteStringPGM((uint16_t)DGUS_Addr::MESSAGE_Line4, msg, DGUS_LINE_LEN, true, true);
      break;
  }
}

void DGUSScreenHandler::SetStatusMessage(const char* msg, const millis_t duration) {
  dgus_display.WriteString((uint16_t)DGUS_Addr::MESSAGE_Status, msg, DGUS_STATUS_LEN, false, true);

  status_expire = (duration > 0 ? ExtUI::safe_millis() + duration : 0);
}

void DGUSScreenHandler::SetStatusMessage(FSTR_P const fmsg, const millis_t duration) {
  dgus_display.WriteStringPGM((uint16_t)DGUS_Addr::MESSAGE_Status, FTOP(fmsg), DGUS_STATUS_LEN, false, true);

  status_expire = (duration > 0 ? ExtUI::safe_millis() + duration : 0);
}

void DGUSScreenHandler::ShowWaitScreen(DGUS_Screen return_screen, bool has_continue) {
  if (return_screen != DGUS_Screen::WAIT) {
    wait_return_screen = return_screen;
  }
  wait_continue = has_continue;

  TriggerScreenChange(DGUS_Screen::WAIT);
}

DGUS_Screen DGUSScreenHandler::GetCurrentScreen() {
  return current_screen;
}

void DGUSScreenHandler::TriggerScreenChange(DGUS_Screen screen) {
  new_screen = screen;
}

void DGUSScreenHandler::TriggerFullUpdate() {
  full_update = true;
}

void DGUSScreenHandler::TriggerEEPROMSave() {
  eeprom_save = ExtUI::safe_millis() + 500;
}

bool DGUSScreenHandler::IsPrinterIdle() {
  return (!ExtUI::commandsInQueue()
          && !ExtUI::isMoving());
}

const DGUS_Addr* DGUSScreenHandler::FindScreenAddrList(DGUS_Screen screen) {
  DGUS_ScreenAddrList list;
  const DGUS_ScreenAddrList *map = screen_addr_list_map;

  do {
    memcpy_P(&list, map, sizeof(*map));
    if (!list.addr_list) break;
    if (list.screen == screen) {
      return list.addr_list;
    }
  } while (++map);

  return nullptr;
}

bool DGUSScreenHandler::CallScreenSetup(DGUS_Screen screen) {
  DGUS_ScreenSetup setup;
  const DGUS_ScreenSetup *list = screen_setup_list;

  do {
    memcpy_P(&setup, list, sizeof(*list));
    if (!setup.setup_fn) break;
    if (setup.screen == screen) {
      return setup.setup_fn();
    }
  } while (++list);

  return true;
}

void DGUSScreenHandler::MoveToScreen(DGUS_Screen screen, bool abort_wait) {
  if (current_screen == DGUS_Screen::KILL) {
    return;
  }

  if (current_screen == DGUS_Screen::WAIT) {
    if (screen != DGUS_Screen::WAIT) {
      wait_return_screen = screen;
    }

    if (!abort_wait) return;

    if (wait_continue && wait_for_user) {
      ExtUI::setUserConfirmed();
    }
  }

  if (!CallScreenSetup(screen)) return;

  if (!SendScreenVPData(screen, true)) {
    DEBUG_ECHOLNPGM("SendScreenVPData failed");
    return;
  }

  current_screen = screen;
  dgus_display.SwitchScreen(current_screen);
}

bool DGUSScreenHandler::SendScreenVPData(DGUS_Screen screen, bool complete_update) {
  if (complete_update) {
    full_update = false;
  }

  const DGUS_Addr *list = FindScreenAddrList(screen);

  while (true) {
    if (!list) return true; // Nothing left to send

    const uint16_t addr = pgm_read_word(list++);
    if (!addr) return true; // Nothing left to send

    DGUS_VP vp;
    if (!DGUS_PopulateVP((DGUS_Addr)addr, &vp)) continue; // Invalid VP
    if (!vp.tx_handler) continue; // Nothing to send
    if (!complete_update && !(vp.flags & VPFLAG_AUTOUPLOAD)) continue; // Unnecessary VP

    uint8_t expected_tx = 6 + vp.size; // 6 bytes header + payload.
    const millis_t try_until = ExtUI::safe_millis() + 1000;

    while (expected_tx > dgus_display.GetFreeTxBuffer()) {
      if (ELAPSED(ExtUI::safe_millis(), try_until)) return false; // Stop trying after 1 second

      dgus_display.FlushTx(); // Flush the TX buffer
      delay(50);
    }

    vp.tx_handler(vp);
  }
}

#endif // DGUS_LCD_UI_RELOADED
