/*
  xsns_79_as608.ino - AS608 and R503 fingerprint sensor support for Tasmota

  Copyright (C) 2021  boaschti and Theo Arends

  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 <http://www.gnu.org/licenses/>.
*/

#ifdef USE_AS608
/*********************************************************************************************\
 * AS608 optical and R503 capacitive Fingerprint sensor
 * - AS608 supports no color leds
 * - R503 v1.1 supports 3 color ring (Red, Blue, Purple)
 * - R503 v1.2 supports 7 color ring (Red, Blue, Purple, Green, Yellow, Cyan, White)
 *
 * Uses Adafruit-Fingerprint-sensor-library with TasmotaSerial
 *
 * Changes made to Adafruit_Fingerprint.h and Adafruit_Fingerprint.cpp:
 * - Replace SoftwareSerial with TasmotaSerial
 * - Add defined(ESP32) where also defined(ESP8266)
\*********************************************************************************************/

#define XSNS_79               79

//#define USE_AS608_MESSAGES

#ifndef AS608_DUPLICATE
#define AS608_DUPLICATE       4    // Number of 0.25 Sec to disable detection
#endif
#ifndef AS608_COLOR_INIT
#define AS608_COLOR_INIT      1    // Red = 1, Blue = 2, Purple = 3, Green = 4, Yellow = 5, Cyan = 6, White = 7
#endif
#ifndef AS608_COLOR_SCAN
#define AS608_COLOR_SCAN      3    // Red = 1, Blue = 2, Purple = 3, Green = 4, Yellow = 5, Cyan = 6, White = 7
#endif

#define D_JSON_FPRINT "FPrint"

#define D_PRFX_FP "Fp"
#define D_CMND_FP_ENROLL "Enroll"
#define D_CMND_FP_DELETE "Delete"
#define D_CMND_FP_COUNT "Count"

const char kAs608Commands[] PROGMEM = D_PRFX_FP "|" D_CMND_FP_ENROLL "|" D_CMND_FP_DELETE "|" D_CMND_FP_COUNT;

void (*const As608Commands[])(void) PROGMEM = { &CmndFpEnroll, &CmndFpDelete, &CmndFpCount };

#ifdef USE_AS608_MESSAGES
const char kAs608Messages[] PROGMEM =
  D_DONE "|" D_FP_PACKETRECIEVEERR "|" D_FP_NOFINGER "|" D_FP_IMAGEFAIL "|" D_FP_UNKNOWNERROR "|" D_FP_IMAGEMESS "|" D_FP_FEATUREFAIL "|" D_FP_NOMATCH "|"
  D_FP_NOTFOUND "|" D_FP_ENROLLMISMATCH "|" D_FP_BADLOCATION "|" D_FP_DBRANGEFAIL "|" D_FP_UPLOADFEATUREFAIL "|" D_FP_PACKETRESPONSEFAIL "|"
  D_FP_UPLOADFAIL "|" D_FP_DELETEFAIL "|" D_FP_DBCLEARFAIL "|" D_FP_PASSFAIL "|" D_FP_INVALIDIMAGE "|" D_FP_FLASHERR "|" D_FP_INVALIDREG "|"
  D_FP_ADDRCODE "|" D_FP_PASSVERIFY;

const uint8_t As608Reference[] PROGMEM = { 0, 1, 2, 3, 4, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 4, 17, 4, 18, 4, 4, 19, 4, 20, 4, 4, 4, 4, 4, 21, 22 };
#else
const char kAs608Messages[] PROGMEM = D_DONE "|" D_FP_UNKNOWNERROR "|" D_FP_NOFINGER;
#endif

#include <TasmotaSerial.h>
#include <Adafruit_Fingerprint.h>

Adafruit_Fingerprint *As608Finger;
TasmotaSerial *As608Serial;

struct AS608 {
  uint16_t finger_id;
  uint16_t confidence;
  bool selected = false;
  uint8_t enroll_step = 0;
  uint8_t model_number = 0;
  uint8_t duplicate;
} As608;

char* As608Message(char* response, uint32_t index) {
#ifdef USE_AS608_MESSAGES
  if (index > sizeof(As608Reference)) { index = 4; }
  uint32_t i = pgm_read_byte(&As608Reference[index]);
#else
  if (index > 2) { index = 1; }
  uint32_t i = index;
#endif
  return GetTextIndexed(response, TOPSZ, i, kAs608Messages);
}

void As608PublishMessage(const char* message) {
  char romram[TOPSZ];
  snprintf_P(romram, sizeof(romram), message);
  if (strlen(romram) > 0) {
    char json_name[20];
    if (As608.enroll_step) {
      strcpy_P(json_name, PSTR(D_PRFX_FP D_CMND_FP_ENROLL));
    } else {
      strcpy_P(json_name, PSTR(D_JSON_FPRINT));
    }
    Response_P(S_JSON_COMMAND_SVALUE, json_name, romram);
    MqttPublishPrefixTopicRulesProcess_P(RESULT_OR_STAT, json_name);
  }
}

void As608Init(void) {
  if (PinUsed(GPIO_AS608_RX) && PinUsed(GPIO_AS608_TX)) {
    As608Serial = new TasmotaSerial(Pin(GPIO_AS608_RX), Pin(GPIO_AS608_TX), 0);
    As608Finger = new Adafruit_Fingerprint(As608Serial, 0);

    As608Finger->begin(57600);
    if (As608Serial->hardwareSerial()) { ClaimSerial(); }
#ifdef ESP32
    AddLog(LOG_LEVEL_DEBUG, PSTR("AS6: Serial UART%d"), As608Serial->getUart());
#endif

    if (As608Finger->verifyPassword()) {
      As608Finger->getTemplateCount();
      AddLog(LOG_LEVEL_INFO, PSTR("AS6: Detected with %d fingerprint(s) stored"), As608Finger->templateCount);
      As608.selected = true;

      As608Finger->LEDcontrol(FINGERPRINT_LED_BREATHING, 100, AS608_COLOR_INIT, 3);
    }
  }
}

int As608GetFingerImage(void) {
  int p = As608Finger->getImage();
  if (p != FINGERPRINT_OK) {
    char response[TOPSZ];
    As608PublishMessage(As608Message(response, p));
  }
  return p;
}

int As608ConvertFingerImage(uint8_t slot) {
  int p = As608Finger->image2Tz(slot);
  if (p != FINGERPRINT_OK) {
    char response[TOPSZ];
    As608PublishMessage(As608Message(response, p));
  }
  return p;
}

void As608Loop(void) {
  if (TasmotaGlobal.uptime < 6) { return; }  // Alow time for initial led breathing
  uint32_t p = 0;

  if (!As608.enroll_step) {
    if (As608.duplicate) {
      As608.duplicate--;
    }
    if (!As608.duplicate) {
      As608Finger->LEDcontrol(FINGERPRINT_LED_OFF, 0, AS608_COLOR_SCAN);
    }

    // Search for Finger
    p = As608Finger->getImage();          // Take image
    if (p != FINGERPRINT_OK) { return; }

    As608Finger->LEDcontrol(FINGERPRINT_LED_GRADUAL_ON, 150, AS608_COLOR_SCAN);

    p = As608Finger->image2Tz();          // Convert image
    if (p != FINGERPRINT_OK) { return; }

//    As608Finger->LEDcontrol(FINGERPRINT_LED_ON, 0, FINGERPRINT_LED_BLUE);

//    p = As608Finger->fingerFastSearch();  // Match found - fails on R503
    p = As608Finger->fingerSearch();      // Match found
    if (p != FINGERPRINT_OK) {
//      AddLog(LOG_LEVEL_DEBUG, PSTR("AS6: No matching finger"));
      Response_P(PSTR("{\"" D_JSON_FPRINT "\":\"NOMATCH\"}"));
      MqttPublishPrefixTopicRulesProcess_P(RESULT_OR_STAT, PSTR(D_JSON_FPRINT));
      return;
    }

    // Found a match
    if (As608.duplicate && (As608.finger_id == As608Finger->fingerID)) {
      return;                             // Skip duplicate during AS608_DUPLICATE * 0.25 second
    }
    As608.duplicate = AS608_DUPLICATE;    // AS608_DUPLICATE * 250mS
    As608Finger->LEDcontrol(FINGERPRINT_LED_ON, 0, AS608_COLOR_SCAN);

    As608.finger_id = As608Finger->fingerID;
    As608.confidence = As608Finger->confidence;
    Response_P(PSTR("{\"" D_JSON_FPRINT "\":{\"" D_JSON_ID "\":%d,\"" D_JSON_CONFIDENCE "\":%d}}"), As608.finger_id, As608.confidence);
    MqttPublishPrefixTopicRulesProcess_P(RESULT_OR_STAT, PSTR(D_JSON_FPRINT));
    return;
  } else {
    // enroll is active
    switch (As608.enroll_step) {
      case 1:
        As608PublishMessage(PSTR(D_FP_ENROLL_PLACEFINGER));
//        As608Finger->LEDcontrol(FINGERPRINT_LED_ON, 0, FINGERPRINT_LED_BLUE);
        As608.enroll_step++;
        break;
      case 2:
        // get first image
        if (As608GetFingerImage() == FINGERPRINT_OK) {
          As608.enroll_step++;
        }
        break;
      case 3:
        // convert image
        if (As608ConvertFingerImage(1) == FINGERPRINT_OK) {
          As608.enroll_step++;
        } else {
          As608.enroll_step--;
        }
        break;
      case 4:
        As608PublishMessage(PSTR(D_FP_ENROLL_REMOVEFINGER));
        As608.enroll_step++;
        break;
      case 5:
        // Remove finger
        p = As608Finger->getImage();
        if (p == FINGERPRINT_NOFINGER) {
          As608.enroll_step++;
        }
        break;
      case 6:
        As608PublishMessage(PSTR(D_FP_ENROLL_PLACESAMEFINGER));
//        As608Finger->LEDcontrol(FINGERPRINT_LED_OFF, 0, FINGERPRINT_LED_PURPLE);
        As608.enroll_step++;
        break;
      case 7:
        // get second image of finger
        if (As608GetFingerImage() == FINGERPRINT_OK) {
          As608.enroll_step++;
        }
        break;
      case 8:
        // convert second image
        if (As608ConvertFingerImage(2) != FINGERPRINT_OK) {
          As608PublishMessage(PSTR(D_FP_ENROLL_RETRY));
          As608.enroll_step -= 2;
          break;
        }
        // Create model
        p = As608Finger->createModel();
        if (p != FINGERPRINT_OK) {
          char response[TOPSZ];
          As608PublishMessage(As608Message(response, p));
          As608.enroll_step = 99;
          break;
        }
        // Store model
        p = As608Finger->storeModel(As608.model_number);
        char response[TOPSZ];
        As608PublishMessage(As608Message(response, p));
        if (p == FINGERPRINT_OK) {
          As608.enroll_step = 0;
          As608.model_number = 0;
        } else {
          As608.enroll_step = 99;
        }
        break;
      case 99:
        As608PublishMessage(PSTR(D_FP_ENROLL_RESTART));
        As608.enroll_step = 1;
        break;
      default:
        As608PublishMessage(PSTR(D_FP_ENROLL_ERROR));
        As608.enroll_step = 0;
        As608.model_number = 0;
        break;
      }
  }
}

/*********************************************************************************************\
* Commands
\*********************************************************************************************/

void CmndFpEnroll(void) {
  if (As608.enroll_step) {
    if (0 == XdrvMailbox.payload) {
      // FpEnroll 0 - Stop enrollement
      As608.enroll_step = 0;
      ResponseCmndChar_P(PSTR(D_FP_ENROLL_RESET));
    } else {
      // FpEnroll - Enrollement state
      ResponseCmndChar_P(PSTR(D_FP_ENROLL_ACTIVE));
    }
  } else {
    if ((XdrvMailbox.payload > 0) && (XdrvMailbox.payload <= 128)) {
      // FpEnroll 1..128 - Start enrollement into slot x
      As608.enroll_step = 1;
      As608.model_number = XdrvMailbox.payload;
      ResponseClear();  // Will use loop start message
    } else {
      // FpEnroll - Enrollement state
      ResponseCmndChar_P(PSTR(D_FP_ENROLL_INACTIVE));
    }
  }
}

void CmndFpDelete(void) {
  if (0 == XdrvMailbox.payload) {
    // FpDelete 0 - Clear database
    As608Finger->emptyDatabase();
    As608Finger->getTemplateCount();
    if (As608Finger->templateCount) {
      ResponseCmndChar_P(PSTR(D_FP_DBCLEARFAIL));
    } else {
      ResponseCmndDone();
    }
  }
  else if ((XdrvMailbox.payload > 0) && (XdrvMailbox.payload <= 128)) {
    // FpDelete 1..128 - Delete single entry from database
    int p = As608Finger->deleteModel(XdrvMailbox.payload);
    char response[TOPSZ];
    ResponseCmndChar(As608Message(response, p));
  }
}

void CmndFpCount(void) {
  // FpCount - Show number of slots used
  As608Finger->getTemplateCount();
  ResponseCmndNumber(As608Finger->templateCount);
}

/*********************************************************************************************\
* Interface
\*********************************************************************************************/

bool Xsns79(uint32_t function) {
  bool result = false;

  if (FUNC_INIT == function) {
    As608Init();
  }
  else if (As608.selected) {
    switch (function) {
      case FUNC_EVERY_250_MSECOND:
        As608Loop();
        break;
#ifdef USE_WEBSERVER
      case FUNC_WEB_SENSOR:
        WSContentSend_PD(PSTR("{s}AS608{m}%d-%d{e}"), As608.finger_id, As608.confidence);
        break;
#endif  // USE_WEBSERVER
      case FUNC_COMMAND:
        result = DecodeCommand(kAs608Commands, As608Commands);
        break;
    }
  }
  return result;
}

#endif  // USE_AS608
