#pragma once

#include <stdint.h>

namespace macropad {

const uint8_t KEY_COUNT = 3;
const uint16_t DEBOUNCE_MS = 15;      // a press counts after 15 ms closed
const uint16_t RELEASE_MS = 60;       // a release only after 60 ms open: short contact dropouts
                                      // (loose Dupont, cold joint) do not end a hold or fake taps
const uint16_t HOLD_MS = 3000;        // hold 3 s: the screen changes at 3 s, before the key is released (Roman, 12.11)
const uint32_t STATS_TIMEOUT_MS = 5000;
const int16_t UNKNOWN = -1000;

// Order of the values in the "S:" line from the PC. RAM_USED10 / RAM_TOTAL10 are in 0.1 GB;
// a line with only the first 6 values (helper before v3) leaves them unknown.
enum Stat : uint8_t { CPU_LOAD, GPU_LOAD, RAM_LOAD, CPU_TEMP, GPU_TEMP, RAM_TEMP, RAM_USED10, RAM_TOTAL10, STAT_COUNT };
const uint8_t STAT_COUNT_OLD = 6;

enum KeyEvent : uint8_t { NONE, TAP, HOLD };

// One switch with INPUT_PULLUP: raw level LOW means pressed.
// TAP on release of a short press, HOLD once when the key stays down for HOLD_MS
// (that press then gives no TAP). Pressing needs DEBOUNCE_MS of a stable level, releasing
// RELEASE_MS: the hold timer survives contact dropouts shorter than that.
struct KeyTracker {
  bool stable = false;
  bool last_raw = false;
  bool hold_sent = false;
  uint32_t changed_at = 0;
  uint32_t pressed_at = 0;

  KeyEvent update(bool pressed_raw, uint32_t now) {
    if (pressed_raw != last_raw) {
      last_raw = pressed_raw;
      changed_at = now;
    }

    if (pressed_raw != stable && now - changed_at >= (pressed_raw ? DEBOUNCE_MS : RELEASE_MS)) {
      stable = pressed_raw;

      if (stable) {
        pressed_at = now;
        hold_sent = false;
        return NONE;
      }

      return hold_sent ? NONE : TAP;
    }

    if (stable && !hold_sent && now - pressed_at >= HOLD_MS) {
      hold_sent = true;
      return HOLD;
    }

    return NONE;
  }
};

inline bool parse_int(const char *&p, int16_t &out) {
  if (*p == '-' && (p[1] < '0' || p[1] > '9')) {
    out = UNKNOWN;
    p++;
    return true;
  }

  bool negative = false;

  if (*p == '-') {
    negative = true;
    p++;
  }

  if (*p < '0' || *p > '9') {
    return false;
  }

  int32_t value = 0;

  while (*p >= '0' && *p <= '9') {
    value = value * 10 + (*p - '0');

    if (value > 9999) {
      return false;
    }

    p++;
  }

  if (*p == '.') {
    p++;

    while (*p >= '0' && *p <= '9') {
      p++;
    }
  }

  out = negative ? -value : value;
  return true;
}

// "S:45,12,63,52,40,-,134,180" -> values in Stat order; "-" means unknown. 6 or 8 values.
// Nothing is changed unless the whole line is valid.
inline bool parse_stats(const char *line, int16_t *out) {
  if (line[0] != 'S' || line[1] != ':') {
    return false;
  }

  const char *p = line + 2;
  int16_t values[STAT_COUNT];
  uint8_t count = 0;

  while (count < STAT_COUNT) {
    if (!parse_int(p, values[count])) {
      return false;
    }

    count++;

    if (*p == '\0') {
      break;
    }

    if (*p != ',') {
      return false;
    }

    p++;
  }

  if (*p != '\0' || (count != STAT_COUNT && count != STAT_COUNT_OLD)) {
    return false;
  }

  for (uint8_t i = count; i < STAT_COUNT; i++) {
    values[i] = UNKNOWN;
  }

  for (uint8_t i = 0; i < STAT_COUNT; i++) {
    out[i] = values[i];
  }

  return true;
}

inline uint8_t put_number(char *out, uint16_t v) {
  char digits[6];
  uint8_t n = 0, pos = 0;

  do {
    digits[n++] = '0' + v % 10;
    v /= 10;
  } while (v > 0);

  while (n > 0) {
    out[pos++] = digits[--n];
  }

  return pos;
}

// "45%", "52\xb0" (degree sign in Latin-1 U8g2 fonts) or "" when unknown: nothing is drawn.
inline void format_value(char *out, int16_t value, const char *suffix) {
  if (value == UNKNOWN) {
    out[0] = '\0';
    return;
  }

  char digits[6];
  uint8_t n = 0;
  bool negative = value < 0;
  uint16_t v = negative ? -value : value;

  do {
    digits[n++] = '0' + v % 10;
    v /= 10;
  } while (v > 0);

  uint8_t pos = 0;

  if (negative) {
    out[pos++] = '-';
  }

  while (n > 0) {
    out[pos++] = digits[--n];
  }

  while (*suffix) {
    out[pos++] = *suffix++;
  }

  out[pos] = '\0';
}


inline bool gb_known(int16_t used10, int16_t total10) {
  return used10 != UNKNOWN && total10 != UNKNOWN && used10 >= 0 && total10 > 0;
}

// "15.1" (one decimal under 100) or "102" from 0.1 GB units.
inline void format_tenths(char *out, int16_t v10) {
  uint8_t pos = 0;

  if (v10 < 1000) {
    pos += put_number(out, v10 / 10);
    out[pos++] = '.';
    out[pos++] = '0' + v10 % 10;
  } else {
    pos += put_number(out, (v10 + 5) / 10);
  }

  out[pos] = '\0';
}

// "/18G" from 0.1 GB units.
inline void format_total(char *out, int16_t total10) {
  out[0] = '/';
  uint8_t pos = 1 + put_number(out + 1, (total10 + 5) / 10);
  out[pos++] = 'G';
  out[pos] = '\0';
}

// RAM in GB from 0.1 GB units: "13.4/18G" (one decimal under 100 GB) or, compact, "13/18G".
// "" when either value is unknown.
inline void format_gb(char *out, int16_t used10, int16_t total10, bool decimal) {
  if (!gb_known(used10, total10)) {
    out[0] = '\0';
    return;
  }

  uint8_t pos = 0;

  if (decimal && used10 < 1000) {
    pos += put_number(out + pos, used10 / 10);
    out[pos++] = '.';
    out[pos++] = '0' + used10 % 10;
  } else {
    pos += put_number(out + pos, (used10 + 5) / 10);
  }

  out[pos++] = '/';
  pos += put_number(out + pos, (total10 + 5) / 10);
  out[pos++] = 'G';
  out[pos] = '\0';
}

// Bar fill in pixels for value in [lo, hi], clamped; 0 for unknown.
inline uint8_t bar_fill(int16_t value, int16_t lo, int16_t hi, uint8_t width) {
  if (value == UNKNOWN || value <= lo) {
    return 0;
  }

  if (value >= hi) {
    return width;
  }

  return (uint8_t)((int32_t)(value - lo) * width / (hi - lo));
}

}  // namespace macropad
