/**
 * \file cimditprofile.h
 * \brief Declaration of the CimditProfile class
 * \author GrumpyDeveloper (Sascha Nitsch)
 * \copyright 2022 Sascha Nitsch
 * Licensed under MIT license
 *
 */
#include "defines.h"

// system includes
#include <Arduino.h>
#include <string.h>
#include <stdlib.h>
#ifndef EXTERNAL_EEPROM
#include <EEPROM.h>
#endif

#ifdef HAVE_DISPLAY
#include "cimditssd1306.h"
#endif

// own includes
#include "cimditprofile.h"

extern CimditSSD1306 display;
extern CimditHAL hal;

/*
  structure of profile in eeprom
  uint16_t total size (including length bytes)
  uint8_t number of profiles
  for each profile:
    uint16_t length length of profile in bytes (including length bytes)
    char profile[] 0-terminated profile name
    uint8_t custom image (0/1)
      if custom image
      uint8_t[512] image data
    uint8_t button_mapping number
    repeat for each mapped button
      uint8_t button number
      <mapping enum> uint8_t mapping type
      int8_t value depending on mapping
    uint8_t axis mapping number
    repeated for each mapped axis
      uint8_t axis number
      <mapping enum> uint8_t mapping type
      int8_t value depending on mapping
    uint8_t rotary mapping number
    repeated for each rotary encoder
      uint8_t encoder number (note, each encoder has 2 buttons: counter-clockwise and clockwise)
      <mapping enum> uint8_t mapping type
      int8_t value depending on mapping
    uint8_t number of macro sequences
    repeated for every macro sequence
      const char[] macro sequence, 0-terminated
*/

CimditProfile::CimditProfile() {
  m_numProfiles = 0;
  for (uint8_t i = 0; i < NUMBER_OF_PROFILES; ++i) {
    m_profiles[i][0] = 0;
    m_mappedRotary[i].m_number = -1;
  }
  m_numMappedButtons = 0;
  m_numMappedAxis = 0;
  m_numMappedRotary = 0;
  m_activeProfile = 0;
  m_selectingProfile = 0;
  m_mouseMoveX = 0;
  m_mouseMoveY = 0;
  m_eepromPosition = 0;
  m_stateTimeout = 0;
  m_numMacros = 0;
  m_macroStart = nullptr;
  m_nextBarUpdate = 0;
  m_dotX = 0;
  m_dotY = 0;
  m_relMouseX = 0;
  m_relMouseY = 0;
  m_displayState = DISPLAY_BLANK;
  m_userTimeout = 0;
  m_stateStarted = 0;
  m_customImage = 0;
  memset(m_userString, 0, sizeof(m_userString));
}

uint8_t CimditProfile::nextUInt8() {
  uint8_t ret;
#ifdef EXTERNAL_EEPROM
  Wire.beginTransmission(EXTERNAL_EEPROM);
  Wire.write((uint8_t)(m_eepromPosition >> 8));   // MSB
  Wire.write((uint8_t)(m_eepromPosition & 0xFF)); // LSB
  Wire.endTransmission();
  Wire.requestFrom(EXTERNAL_EEPROM, 1);
  ret =  Wire.read();
  ++m_eepromPosition;
  return ret;
#else
  return EEPROM.read(m_eepromPosition++);
#endif
}

uint16_t CimditProfile::nextUInt16() {
  return ((uint16_t)(nextUInt8()) << 8) + nextUInt8();
}

void CimditProfile::nextString(unsigned char* destination) {
  uint8_t next = nextUInt8();
  while (next != 0) {
    *destination = next;
    next = nextUInt8();
    ++destination;
  }
  *destination = 0;
}

void CimditProfile::begin() {
  initProfiles();
}

void CimditProfile::initProfiles() {
  m_eepromPosition = 2;
  // number of profiles
  m_numProfiles = nextUInt8();
  if (m_numProfiles > NUMBER_OF_PROFILES) m_numProfiles = 0;
  for (uint8_t i = 0; i < m_numProfiles; ++i) {
    uint16_t position = m_eepromPosition;
    uint16_t size = nextUInt16();
    nextString((unsigned char*)(m_profiles[i]));
    m_eepromPosition = position + size;  // we only scan the names here
  }
  if (m_numProfiles>0) {
    load(0);
  }
}

void CimditProfile::load(uint8_t num) {
  m_activeProfile = num;
  m_selectingProfile = num;
  // skip number of profiles
  m_eepromPosition = 3;
  // seek to right profile
  for (uint8_t i = 0; i < num; ++i) {
    // get profile length
    uint16_t size = nextUInt16();
    m_eepromPosition += size - 2;
  }
  // ignore 2 bytes size
  m_eepromPosition += 2;
  // m_eepromPosition points to our profile to parse
  // name is already saved, skip it
  while (nextUInt8() != 0) {
  }
  
  // do we have a custom image?
  m_customImage = nextUInt8() > 0 ? m_eepromPosition : 0;
  if (m_customImage) {
    // skip 512 bytes
    m_eepromPosition += 512;
  }
  // mapped buttons
  // number of mapped buttons
  m_numMappedButtons = nextUInt8();
  if (m_numMappedButtons) {
    for (uint8_t i = 0; i < m_numMappedButtons ; ++i) {
      m_mappedButtons[i].m_number = nextUInt8();
      m_mappedButtons[i].m_type = (MappingType)(nextUInt8());
      m_mappedButtons[i].m_value = nextUInt8();
    }
  }
  // end of mapped buttons

  // mapped axis
  // number of mapped axis
  m_numMappedAxis = nextUInt8();
  if (m_numMappedAxis) {
    for (uint8_t i = 0; i < m_numMappedAxis ; ++i) {
      m_mappedAxis[i].m_number = nextUInt8();
      m_mappedAxis[i].m_type = (MappingType)(nextUInt8());
      m_mappedAxis[i].m_value = nextUInt8();
    }
  }
  // end of mapped axis

  // mapped rotary
  // number of mapped rotary
  m_numMappedRotary = nextUInt8();
  if (m_numMappedRotary) {
    for (uint8_t i = 0; i < m_numMappedRotary ; ++i) {
      m_mappedRotary[i].m_number = nextUInt8();
      m_mappedRotary[i].m_type = (MappingType)(nextUInt8());
      m_mappedRotary[i].m_value = nextUInt8();
    }
  }
  // end of mapped rotary
  m_mouseMoveX = 0;
  m_mouseMoveY = 0;
  // parse macro
  m_numMacros = nextUInt8();
  if (m_macroStart) {
    free(m_macroStart);
  }
  m_macroStart = reinterpret_cast<uint16_t*>(malloc(m_numMacros * sizeof(uint16_t)));
  for (uint8_t macroNum = 0; macroNum < m_numMacros; ++macroNum) {
    m_macroStart[macroNum] = m_eepromPosition;
    scanOrExecuteMacro(macroNum, false);
  }
  showActiveProfile();
}

void CimditProfile::scanOrExecuteMacro(uint8_t macroNum, bool execute) {
  m_eepromPosition = m_macroStart[macroNum];
  uint8_t value8 = 0;
  uint16_t delayTime = 0;
  MACROCOMMANDS token;
  do {
    token = (MACROCOMMANDS)nextUInt8();
    switch (token) {
      case MACRO_NULL:
      break;
      case MACRO_SPEED:  // read in 1 extra numberic value 8 bit
        delayTime = 1000 / nextUInt8();
      break;
      case MACRO_JOY_PRESS:  // read in 1 extra numberic value 8 bit
        value8 = nextUInt8();
        if (execute) {
          Gamepad.press(value8);
          Gamepad.write();
        }
      break;
      case MACRO_JOY_RELEASE:  // read in 1 extra numberic value 8 bit
        value8 = nextUInt8();
        if (execute) {
          Gamepad.release(value8);
          Gamepad.write();
        }
      break;
      case MACRO_MOUSE_PRESS:  // read in 1 extra numberic value 8 bit
        value8 = nextUInt8();
        if (execute) {
          Mouse.press(value8);
        }
      break;
      case MACRO_MOUSE_RELEASE:  // read in 1 extra numberic value 8 bit
        value8 = nextUInt8();
        if (execute) {
          Mouse.release(value8);
        }
        break;
      case MACRO_MOUSE_WHEEL:   // read in 1 extra numberic value 8 bit
        value8 = nextUInt8();
        if (execute) {
          Mouse.move(0, 0, value8);
        }
      break;
      case MACRO_DELAY:  // read in 1 extra numberic value 16 bit
        if (execute) {
          delay(nextUInt16());
        }
      break;
      case MACRO_KEY_PRESS:  // one or more keys
        while ((value8 = nextUInt8()) != MACRO_END) {
          if (execute) {
            Keyboard.press((KeyboardKeycode)value8);
            if (delayTime) delay(delayTime);
          }
        }
      break;
      case MACRO_KEY_RELEASE:
        while ((value8 = nextUInt8()) != MACRO_END) {
          if (execute) {
            Keyboard.release((KeyboardKeycode)value8);
            if (delayTime) delay(delayTime);
          }
        }
      break;
      case MACRO_TYPE:
        while ((value8 = nextUInt8()) != MACRO_END) {
          if (execute) {
            Keyboard.write((KeyboardKeycode)value8);
            if (delayTime) delay(delayTime);
          }
        }
      break;
      case MACRO_MOUSE_REL_X:  // read in 1 extra numberic value 8 bit
        value8 = nextUInt8();
        if (execute) {
          Mouse.move(value8, 0, 0);
        }
      break;
      case MACRO_MOUSE_REL_Y:  // read in 1 extra numberic value 8 bit
        value8 = nextUInt8();
        if (execute) {
          Mouse.move(0, value8, 0);
        }
      break;
      case MACRO_MOUSE_REL_X_AXIS: {  // one or 2 bytes"
        int8_t axis = nextUInt8();
        if (axis != -1) {
          value8 = nextUInt8();
          if (execute) {
            // find previous mapping
            bool found = false;
            for (uint8_t i = 0; i < m_numMappedAxis; ++i) {
              if (m_mappedAxis[i].m_number == axis) {
                m_mappedAxis[i].m_type = MOUSE_REL_X_AXIS;
                m_mappedAxis[i].m_value = value8;
                found = true;
              }
            }
            if (!found) {
              m_mappedAxis[m_numMappedAxis].m_type = MOUSE_REL_X_AXIS;
              m_mappedAxis[m_numMappedAxis].m_number = axis;
              m_mappedAxis[m_numMappedAxis++].m_value = value8;
            }
          }
        } else {
          if (execute) {
            // find axis
            for (uint8_t i = 0; i < m_numMappedAxis; ++i) {
              if (m_mappedAxis[i].m_number == axis) {
                m_mappedAxis[i].m_type = MAPPING_NONE;
              }
            }
          }
        }
      }
      break;
      case MACRO_MOUSE_REL_Y_AXIS: {  // one or 2 bytes
        int8_t axis = nextUInt8();
        if (axis != -1) {
          value8 = nextUInt8();
          if (execute) {
            // find previous mapping
            bool found = false;
            for (uint8_t i = 0; i < m_numMappedAxis; ++i) {
              if (m_mappedAxis[i].m_number == axis) {
                m_mappedAxis[i].m_type = MOUSE_REL_Y_AXIS;
                m_mappedAxis[i].m_value = value8;
                found = true;
              }
            }
            if (!found) {
              m_mappedAxis[m_numMappedAxis].m_type = MOUSE_REL_Y_AXIS;
              m_mappedAxis[m_numMappedAxis].m_number = axis;
              m_mappedAxis[m_numMappedAxis++].m_value = value8;
            }
          }
        } else {
          if (execute) {
            // find axis
            for (uint8_t i = 0; i < m_numMappedAxis; ++i) {
              if (m_mappedAxis[i].m_number == axis) {
                m_mappedAxis[i].m_type = MAPPING_NONE;
              }
            }
          }
        }
      }
      break;
    }
  } while (token != MACRO_NULL);
}

void CimditProfile::rotaryAction(uint8_t num, int8_t delta) {
  if (m_displayState != DISPLAY_USER_STRING && m_displayState != DISPLAY_CONFIRMATION) {
    showActiveProfile();
  }
  // find right action
  // rotary has counter-clockwise and clockwise mapping
  num = (num << 1) + (delta > 0);
  for (uint8_t i = 0; i < m_numMappedRotary; ++i) {
    if (m_mappedRotary[i].m_number == num) {
      // rotary supports mouse rel axis, next/prev profile and macro
      switch (m_mappedRotary[i].m_type) {
        case MOUSE_REL_X_AXIS:
          Mouse.move(m_mappedRotary[i].m_value * delta, 0, 0);
          break;
        case MOUSE_REL_Y_AXIS:
          Mouse.move(0, m_mappedRotary[i].m_value * delta, 0);
          break;
        case MOUSE_REL_WHEEL:
          Mouse.move(0, 0, m_mappedRotary[i].m_value * delta);
          break;
        case NEXT_PROFILE:
        case PREV_PROFILE:
          if (m_mappedRotary[i].m_type == NEXT_PROFILE) {
            m_selectingProfile = (m_selectingProfile + 1) % m_numProfiles;
          } else {
            if (m_selectingProfile > 0) {
              --m_selectingProfile;
            } else {
              m_selectingProfile = m_numProfiles - 1;
            }
          }
          showActivate();
          break;
        default:
          break;
      }
    }
  }
}

void CimditProfile::showActiveProfile() {
  if (m_displayState != DISPLAY_PROFILE) {
#ifdef HAVE_DISPLAY
    if (m_customImage) {
      m_eepromPosition = m_customImage;
      for (uint8_t y = 0; y < SCREEN_HEIGHT; ++y) {
        for (uint8_t x = 0; x < SCREEN_WIDTH; x += 8) {
          uint8_t img = nextUInt8();
          for (uint8_t t = 0; t < 8; ++t) {
            display.drawPixel(x + t, y, (img >> (7-t)) & 1);
          }
        }
      }
    } else {
      printTextCentered(m_profiles[m_activeProfile]);
    }
#else
    Serial.println(m_profiles[m_activeProfile]);
#endif
    m_displayState = DISPLAY_PROFILE;
    display.display();
  }
  m_stateTimeout = hal.m_millis + PROFILE_TIME;
  m_stateStarted = hal.m_millis;
}

void CimditProfile::showActivate() {
#ifdef HAVE_DISPLAY
  display.clearDisplay();
  display.setTextSize(2,2);
  display.setCursor(16, 0);
  display.write("activate");
  // default font is 5x8 pixel
  uint8_t w = (strlen(m_profiles[m_selectingProfile]) + 1) * 2 * 6;
  uint8_t h = 16;
  if (w > SCREEN_WIDTH) {
    display.setTextSize(1,1);
    w = w >> 1;
    h = 8;
  }
  display.setCursor((SCREEN_WIDTH - w) >> 1, (SCREEN_HEIGHT >> 1) + ((SCREEN_HEIGHT - h)>> 1));
  display.write(m_profiles[m_selectingProfile]);
  display.write("?");
  display.display();
#else
  Serial.print(F("activate "));
  Serial.print(m_profiles[m_selectingProfile]);
  Serial.println(F("?"));
#endif
  m_displayState = DISPLAY_CONFIRMATION;
  m_stateTimeout = hal.m_millis + SELECTION_TIME;
  m_stateStarted = hal.m_millis;
}

void CimditProfile::printTextCentered(const char* text) {
  display.clearDisplay();
  display.setTextSize(2,2);
  // default font is 6x8 pixel
  uint8_t w = strlen(text) * 12;
  uint8_t h = 16;
  if (w > SCREEN_WIDTH) {
    display.setTextSize(1,1);
    w = w >> 1;
    h = 8;
  }
  display.setCursor((SCREEN_WIDTH - w) >> 1, (SCREEN_HEIGHT - h) >> 1);
  display.write(text);
  display.display();
}

void CimditProfile::showUserString() {
#ifdef HAVE_DISPLAY
  printTextCentered(m_userString);
#endif
  m_displayState = DISPLAY_USER_STRING;
  m_stateTimeout = hal.m_millis + m_userTimeout;
  m_stateStarted = hal.m_millis;
}

void CimditProfile::axisAction(uint8_t num, uint16_t state) {
  for (uint8_t i = 0; i < m_numMappedAxis; ++i) {
    if (m_mappedAxis[i].m_number == num) {
      switch (m_mappedAxis[i].m_type) {
        case JOYSTICK_AXIS:
         switch (m_mappedAxis[i].m_value) {
          case 1:
            Gamepad.xAxis(map(state, 0, 1023, -32768, 32767));
            break;
          case 2:
            Gamepad.yAxis(map(state, 0, 1023, -32768, 32767));
            break;
          case 3:
            Gamepad.zAxis(map(state, 0, 1023, -128, 127));
            break;
          case 4:
            Gamepad.rxAxis(map(state, 0, 1023, -32768, 32767));
            break;
          case 5:
            Gamepad.ryAxis(map(state, 0, 1023, -32768, 32767));
            break;
          case 6:
            Gamepad.rzAxis(map(state, 0, 1023, -128, 127));
            break;
         }
        break;
        case MOUSE_REL_X_AXIS: {
            int16_t pos = (int16_t)state - 512;
            if (pos > JOYSTICK_DEAD_ZONE_X) {
              m_mouseMoveX = map(pos, JOYSTICK_DEAD_ZONE_X, 511, 0, m_mappedAxis[i].m_value);
            } else if (pos < -JOYSTICK_DEAD_ZONE_X) {
              m_mouseMoveX = map(pos, -512, -JOYSTICK_DEAD_ZONE_X, -m_mappedAxis[i].m_value, 0);
            } else {
              m_mouseMoveX = 0;
            }
          }
        break;
        case MOUSE_REL_Y_AXIS: {
            int16_t pos = (int16_t)state - 512;
            if (pos > JOYSTICK_DEAD_ZONE_Y) {
              m_mouseMoveY = map(pos, JOYSTICK_DEAD_ZONE_Y, 511, 0, m_mappedAxis[i].m_value);
            } else if (pos < -JOYSTICK_DEAD_ZONE_Y) {
              m_mouseMoveY = map(pos, -512, -JOYSTICK_DEAD_ZONE_Y, -m_mappedAxis[i].m_value, 0);
            } else {
              m_mouseMoveY = 0;
            }
          }

        break;
        default:
        break;
      }
    }
  }
}

void CimditProfile::buttonAction(uint8_t num, bool state) {
  if (m_displayState != DISPLAY_USER_STRING && m_displayState != DISPLAY_CONFIRMATION) {
    showActiveProfile();
  }
  for (uint8_t i = 0; i < m_numMappedButtons; ++i) {
    if (m_mappedButtons[i].m_number == num) {
      switch (m_mappedButtons[i].m_type) {
        case JOYSTICK_BUTTON:
          if (state) {
            Gamepad.press(m_mappedButtons[i].m_value);
          } else {
            Gamepad.release(m_mappedButtons[i].m_value);
          }
          Gamepad.write();
        break;
        case MOUSE_BUTTON:
          if (state) {
            Mouse.press(m_mappedButtons[i].m_value);
          } else {
            Mouse.release(m_mappedButtons[i].m_value);
          }
        break;
        case MOUSE_REL_X_AXIS:
          Mouse.move(m_mappedButtons[i].m_value, 0, 0);
        break;
        case MOUSE_REL_Y_AXIS:
          Mouse.move(0, m_mappedButtons[i].m_value, 0);
        break;
        case MOUSE_REL_WHEEL:
          Mouse.move(0, 0, m_mappedButtons[i].m_value);
        break;
        case NEXT_PROFILE:
        case PREV_PROFILE:
          if (m_mappedRotary[i].m_type == NEXT_PROFILE) {
            m_selectingProfile = (m_selectingProfile + 1) % m_numProfiles;
          } else {
            if (m_selectingProfile > 0) {
              --m_selectingProfile;
            } else {
              m_selectingProfile = m_numProfiles - 1;
            }
          }
          showActivate();
          break;
        case SWITCH_PROFILE:
          if (m_stateTimeout && state && m_displayState == DISPLAY_CONFIRMATION) {
            load(m_selectingProfile);
          }
        break;
        case MACRO_PRESS:
          if (state)
            scanOrExecuteMacro(m_mappedButtons[i].m_value, true);
        break;
        case MACRO_RELEASE:
          if (!state)
            scanOrExecuteMacro(m_mappedButtons[i].m_value, true);
        break;
        case KEYBOARD_BUTTON:
          if (state) {
            Keyboard.press((KeyboardKeycode)m_mappedButtons[i].m_value);
          } else {
            Keyboard.release((KeyboardKeycode)m_mappedButtons[i].m_value);
          }
        break;
        case MAPPING_NONE:
        case JOYSTICK_AXIS:
        break;
      }
    }
  }
}

void CimditProfile::tick() {
  if (m_stateTimeout && hal.m_millis >= m_stateTimeout) {
    switch (m_displayState) {
      case DISPLAY_BLANK: // ignored
      break;
      case DISPLAY_PROFILE: // go to blank display state
        display.clearDisplay();
        display.display();
        m_displayState = DISPLAY_BLANK;
        m_stateTimeout = 0;
        m_stateStarted = hal.m_millis;
      break;
      case DISPLAY_CONFIRMATION:
      case DISPLAY_USER_STRING:
        // go back to profile name
        showActiveProfile();
        m_selectingProfile = m_activeProfile;
      break;
    }
  }
  if (hal.m_millis > m_nextBarUpdate) {
    m_nextBarUpdate = hal.m_millis + BAR_UPDATE_TIME;
    if (m_stateTimeout) {
      uint8_t percentage = SCREEN_WIDTH * (hal.m_millis - m_stateStarted) / (m_stateTimeout - m_stateStarted);
      // white bar from left to percentage
      display.drawFastHLine(0, SCREEN_HEIGHT-1, percentage, true);
      // black bar from percentage to right
      display.drawFastHLine(percentage + 1, SCREEN_HEIGHT-1, SCREEN_WIDTH - percentage - 1, false);
      display.displayPartial(3);
    }
    if (m_displayState == DISPLAY_BLANK) {
      // clear old pixel
      display.drawPixel(m_dotX, m_dotY, false);
      uint8_t part1 = m_dotY/8;
      uint16_t r = rand();
      m_dotX = r & 127;
      m_dotY = r & 31;
      display.drawPixel(m_dotX, m_dotY, true);
      uint8_t part2 = m_dotY/8;
      if (part1 != part2) {
        display.displayPartial(part2);
      }
      display.displayPartial(part1);
    }
    
  }
  if (m_mouseMoveX || m_mouseMoveY) {
    m_relMouseX += (float)m_mouseMoveX / 50;
    m_relMouseY += (float)m_mouseMoveY / 50;
    if (fabs(m_relMouseX) >= 1 || fabs(m_relMouseY) >= 1) {
      Mouse.move(m_relMouseX, m_relMouseY);
      m_relMouseX -= (int8_t)m_relMouseX;
      m_relMouseY -= (int8_t)m_relMouseY;
    }
  }
}

void CimditProfile::printFlash() {
  const uint8_t eepromPosition = m_eepromPosition;
  m_eepromPosition = 0;
  uint16_t len = nextUInt16();
  m_eepromPosition = 0;
  if (len < 1024) {
    uint8_t in;
    for (uint16_t i = 0; i < len; ++i) {
      in = nextUInt8();
      Serial.print(in >> 8, 16);
      Serial.print(in & 255, 16);
    }
    Serial.println();
  }
  m_eepromPosition = eepromPosition;
}

#ifdef EXTERNAL_EEPROM
void CimditProfile::eepromWrite(uint16_t addr, uint8_t val) {
  Wire.beginTransmission(EXTERNAL_EEPROM);
  Wire.write((uint8_t)(addr >> 8));   // MSB
  Wire.write((uint8_t)(addr & 0xFF)); // LSB
  Wire.write(val);
  Wire.endTransmission();
  delay(5);
}
#endif

void CimditProfile::writeFlash() {
  char buf[5]= {0};
  Serial.readBytes(buf, 4);
  uint16_t len = strtol(buf, NULL, 16);
  buf[2] = 0;
  m_eepromPosition = 0;
#ifdef EXTERNAL_EEPROM
  eepromWrite(m_eepromPosition++, len >> 8);
  eepromWrite(m_eepromPosition++, len & 255);
 #else
  EEPROM.write(m_eepromPosition++, len >> 8);
  EEPROM.write(m_eepromPosition++, len & 255);
#endif
  if (len < 1024) {
    for (uint16_t i = 2; i < len; ++i) {
      while ((buf[0] = Serial.read()) == -1) {
        delay(100);
      }
      while ((buf[1] = Serial.read()) == -1) {
        delay(100);
      }
      uint8_t in = strtol(buf, NULL, 16);
#ifdef EXTERNAL_EEPROM
      eepromWrite(m_eepromPosition++, in);
#else
      EEPROM.write(m_eepromPosition++, in);
#endif
    }
    Serial.println("written");
    initProfiles();
  }
}

void CimditProfile::setUserString(uint8_t timeout, const char* input) {
  strncpy(m_userString, input, sizeof(m_userString) - 1);
  m_userTimeout = timeout * 1000;
  showUserString();
}

void CimditProfile::userDisplay() {
  uint8_t timeout = 0; 
  char buf = 0;
  // read number for time to display
  do {
    buf = Serial.read();
    if (buf != '\n') {
      timeout = timeout * 10 + buf - '0';
    }
  } while (buf != '\n');
  Serial.println(F("ready for .pbm file"));
  // read header
  do {
    buf = Serial.read();
  } while (buf != '\n');
  while (!Serial.available()) { delay(10); };
  buf = Serial.read();
  if (buf == '#') { // a comment
    do {
      buf = Serial.read();
    } while (buf != '\n');
  }
  while (!Serial.available()) { delay(10); };
  // width height
  uint8_t width = 0;
  do {
    buf = Serial.read();

    if (buf != ' ') {
      width = width * 10 + buf - '0';
    }
  } while (buf != ' ');
  uint8_t height = 0;
  do {
    buf = Serial.read();
    if (buf != '\n') {
      height = height * 10 + buf - '0';
    }
  } while (buf != '\n');
  buf = 0;
  uint16_t len = width * height;
  if (width != SCREEN_WIDTH || SCREEN_HEIGHT != 32) {
    for (uint8_t i = 0; i < width * height; ++i) {
      buf = Serial.read();
      if (buf == '\n') buf = Serial.read();
    }
    return;
  }
  for (uint8_t y = 0; y < SCREEN_HEIGHT; ++y) {
    for (uint8_t x = 0; x < SCREEN_WIDTH; ++x) {
      while ((buf = Serial.read()) == -1) {
        delay(100);
      }
      while (buf != '0' && buf != '1') {
        buf = Serial.read();
      }
      display.drawPixel(x, y, buf == '1');
    }
  }
  display.display();
  m_displayState = DISPLAY_USER_STRING;
  uint32_t now = millis();
  m_stateTimeout = now + (uint32_t)timeout * 1000;
  m_stateStarted = now;
}
