/****************************************************************************
 * This file is part of the project AqHome.
 * AqHome (c) by 2023 Martin Preuss, all rights reserved.
 *
 * The license for this file can be found in the file COPYING which you
 * should have received along with this file.
 ****************************************************************************/

#ifdef HAVE_CONFIG_H
# include <config.h>
#endif

#include "./flash.h"
#include "../utils.h"

#include "aqhome/ipc/endpoint_ipc.h"
#include "aqhome/ipc/nodes/msg_ipc_setaccmsggrps.h"
#include "aqhome/ipc/nodes/msg_ipc_forward.h"
#include "aqhome/msg/msg_node.h"
#include "aqhome/msg/msg_flashready.h"
#include "aqhome/msg/msg_flashstart.h"
#include "aqhome/msg/msg_flashresponse.h"
#include "aqhome/msg/msg_flashdata.h"
#include "aqhome/msg/msg_flashend.h"
#include "aqhome/msg/msg_reboot.h"
#include "aqhome/hexfile/hexfile.h"
#include "aqhome/hexfile/flashrecord.h"

#include <gwenhywfar/args.h>
#include <gwenhywfar/i18n.h>
#include <gwenhywfar/debug.h>
#include <gwenhywfar/text.h>

#include <time.h>
#include <unistd.h>


#define I18S(msg) msg
#define I18N(msg) GWEN_I18N_Translate(PACKAGE, msg)

#define FLASH_TOOL_MAX_REPEAT 16

/*#define DEBUG_FLASH*/


static int _doFlash(GWEN_DB_NODE *dbArgs);

static AQH_FLASHRECORD_LIST *_readHexfileIntoFlashRecordList(const char *hexFilename);

static int _rebootNode(GWEN_MSG_ENDPOINT *epTcp, unsigned int uid, int timeoutInSeconds);
static int _performFlashProcedure(GWEN_MSG_ENDPOINT *epTcp,
                                  unsigned int uid,
                                  const AQH_FLASHRECORD_LIST *flashRecordList,
                                  int pageSize,
                                  int timeoutInSeconds);


static int _flashStart(GWEN_MSG_ENDPOINT *epTcp, unsigned int uid, int timeoutInSeconds);


static GWEN_MSG *_waitForFlashReadyMessageForUid(GWEN_MSG_ENDPOINT *epTcp,
                                                 unsigned int uid, int timeoutInSeconds);
static int _sendRebootRequest(GWEN_MSG_ENDPOINT *epTcp, unsigned int uid);
static int _sendFlashStart(GWEN_MSG_ENDPOINT *epTcp, unsigned int uid);
static int _waitForFlashResponseMessage(GWEN_MSG_ENDPOINT *epTcp, int timeoutInSeconds);
static int _waitForRebootResponseMessage(GWEN_MSG_ENDPOINT *epTcp, int timeoutInSeconds);
static int _sendFlashRecord(GWEN_MSG_ENDPOINT *epTcp,
                            const AQH_FLASHRECORD *flashRecord,
                            uint16_t pageSize,
                            int timeoutInSeconds);
static int _sendFlashEnd(GWEN_MSG_ENDPOINT *epTcp, int reason);



int AQH_Tool_Flash(GWEN_DB_NODE *dbGlobalArgs, int argc, char **argv)
{
  GWEN_DB_NODE *dbLocalArgs;
  int rv;
  const GWEN_ARGS args[]= {
    {
      GWEN_ARGS_FLAGS_HAS_ARGUMENT, /* flags */
      GWEN_ArgsType_Char,           /* type */
      "tcpAddress",                 /* name */
      0,                            /* minnum */
      1,                            /* maxnum */
      "t",                          /* short option */
      "tcpaddress",                 /* long option */
      I18S("Specify TCP address to connect to (defaults to 127.0.0.1)"),
      I18S("Specify TCP address to connect to (defaults to 127.0.0.1)")
    },
    {
      GWEN_ARGS_FLAGS_HAS_ARGUMENT, /* flags */
      GWEN_ArgsType_Int,            /* type */
      "tcpPort",                    /* name */
      0,                            /* minnum */
      1,                            /* maxnum */
      "P",                          /* short option */
      "tcpport",                    /* long option */
      I18S("Specify the TCP port to listen on"),
      I18S("Specify the TCP port to listen on")
    },
    {
      GWEN_ARGS_FLAGS_HAS_ARGUMENT, /* flags */
      GWEN_ArgsType_Char,           /* type */
      "uid",                        /* name */
      1,                            /* minnum */
      1,                            /* maxnum */
      "u",                          /* short option */
      "uid",                        /* long option */
      I18S("Specify UID of the node to flash"),
      I18S("Specify UID of the node to flash)")
    },
    {
      GWEN_ARGS_FLAGS_HAS_ARGUMENT, /* flags */
      GWEN_ArgsType_Char,           /* type */
      "hexFilename",                /* name */
      1,                            /* minnum */
      1,                            /* maxnum */
      "H",                          /* short option */
      NULL,                         /* long option */
      I18S("Specify hexfile to flash"),
      I18S("Specify hexfile to flash)")
    },
    {
      GWEN_ARGS_FLAGS_HAS_ARGUMENT, /* flags */
      GWEN_ArgsType_Int,            /* type */
      "timeout",                    /* name */
      0,                            /* minnum */
      1,                            /* maxnum */
      "T",                          /* short option */
      NULL,                         /* long option */
      I18S("Specify timeout in seconds for PONG response"),
      I18S("Specify timeout in seconds for PONG response")
    },
    {
      0, /* flags */
      GWEN_ArgsType_Int,            /* type */
      "reboot",                    /* name */
      0,                            /* minnum */
      1,                            /* maxnum */
      "R",                          /* short option */
      NULL,                         /* long option */
      I18S("Request node reboot before trying to flash"),
      I18S("Request node reboot before trying to flash")
    },
    {
      GWEN_ARGS_FLAGS_HELP | GWEN_ARGS_FLAGS_LAST, /* flags */
      GWEN_ArgsType_Int,             /* type */
      "help",                       /* name */
      0,                            /* minnum */
      0,                            /* maxnum */
      "h",                          /* short option */
      "help",                       /* long option */
      "Show this help screen",      /* short description */
      "Show this help screen"       /* long description */
    }
  };

  dbLocalArgs=GWEN_DB_GetGroup(dbGlobalArgs, GWEN_DB_FLAGS_DEFAULT, "local");
  rv=GWEN_Args_Check(argc, argv, 1,
                     GWEN_ARGS_MODE_ALLOW_FREEPARAM,
                     args,
                     dbLocalArgs);
  if (rv==GWEN_ARGS_RESULT_ERROR) {
    fprintf(stderr, "ERROR: Could not parse arguments\n");
    return 1;
  }
  else if (rv==GWEN_ARGS_RESULT_HELP) {
    GWEN_BUFFER *ubuf;

    ubuf=GWEN_Buffer_new(0, 1024, 0, 1);
    if (GWEN_Args_Usage(args, ubuf, GWEN_ArgsOutType_Txt)) {
      fprintf(stderr, "ERROR: Could not create help string\n");
      return 1;
    }
    fprintf(stderr, "%s\n", GWEN_Buffer_GetStart(ubuf));
    GWEN_Buffer_free(ubuf);
    return 0;
  }

  return _doFlash(dbLocalArgs);
}



int _doFlash(GWEN_DB_NODE *dbArgs)
{
  GWEN_MSG_ENDPOINT *epTcp;
  int rv;
  int timeoutInSeconds;
  int doReboot;
  const char *s;
  unsigned int uid;
  const char *hexFilename;
  AQH_FLASHRECORD_LIST *flashRecordList=NULL;
  GWEN_MSG *msg;
  uint16_t pageSize;

  /* read data */
  timeoutInSeconds=GWEN_DB_GetIntValue(dbArgs, "timeout", 0, 30);
  doReboot=GWEN_DB_GetIntValue(dbArgs, "reboot", 0, 0);
  s=GWEN_DB_GetCharValue(dbArgs, "uid", 0, NULL);
  if (1!=sscanf(s, "%x", &uid)) {
    DBG_ERROR(NULL, "Bad uid \"%s\"", s);
    return 1;
  }
  hexFilename=GWEN_DB_GetCharValue(dbArgs, "hexFilename", 0, NULL);
  flashRecordList=_readHexfileIntoFlashRecordList(hexFilename);
  if (flashRecordList==NULL) {
    DBG_ERROR(NULL, "Error reading flash record list from hexfile");
    return 2;
  }

  /* setup client connection */
  epTcp=Utils_SetupNodesClientEndpoint(dbArgs);
  if (epTcp==NULL) {
    DBG_ERROR(NULL, "ERROR creating TCP connection");
    return 3;
  }

  /* declare accepted message type groups */
  rv=Utils_SendAcceptedMsgGroups(epTcp, AQH_MSG_TYPEGROUP_FLASH);
  if (rv<0) {
    DBG_INFO(NULL, "here (%d)", rv);
    GWEN_MsgEndpoint_free(epTcp);
    return 3;
  }

  if (doReboot) {
    fprintf(stdout, "Sending REBOOT request\n");
    rv=_rebootNode(epTcp, uid, timeoutInSeconds);
    if (rv<0) {
      DBG_INFO(NULL, "here (%d)", rv);
      GWEN_MsgEndpoint_free(epTcp);
      return 3;
    }
    fprintf(stdout, "Reboot in progress\n");
  }

  /* wait for FLASH_READY message */
  fprintf(stdout, "Waiting for node to become ready for flashing\n");
  msg=_waitForFlashReadyMessageForUid(epTcp, uid, timeoutInSeconds);
  if (msg==NULL) {
    DBG_INFO(NULL, "No FLASH_READY message received.");
    GWEN_MsgEndpoint_free(epTcp);
    return 3;
  }
  DBG_INFO(NULL, "FLASH_READY message received");
  pageSize=AQH_FlashReadyMsg_GetPagesize(msg);
  fprintf(stdout, "Node is ready for flashing (pagesize=%d bytes)\n", pageSize);
  GWEN_Msg_free(msg);

  /* perform flash */
  rv=_performFlashProcedure(epTcp, uid, flashRecordList, pageSize, timeoutInSeconds);
  if (rv<0) {
    if (rv<0) {
      DBG_INFO(NULL, "here (%d)", rv);
      GWEN_MsgEndpoint_free(epTcp);
      AQH_FlashRecord_List_free(flashRecordList);
      return 4;
    }
  }

  AQH_FlashRecord_List_free(flashRecordList);
  GWEN_MsgEndpoint_free(epTcp);
  return 0;
}



AQH_FLASHRECORD_LIST *_readHexfileIntoFlashRecordList(const char *hexFilename)
{
  AQH_HEXFILE *hexfile;
  AQH_FLASHRECORD_LIST *flashRecordList;

  fprintf(stdout, "Reading hexfile \"%s\"\n", hexFilename);

  /* read hexfile */
  hexfile=AQH_Hexfile_fromFile(hexFilename);
  if (hexfile==NULL) {
    DBG_ERROR(NULL, "Error loading hexfile \"%s\"", hexFilename);
    return NULL;
  }
  flashRecordList=AQH_FlashRecord_fromHexfileRecords(AQH_Hexfile_GetRecordList(hexfile));
  AQH_Hexfile_free(hexfile);
  if (flashRecordList==NULL) {
    DBG_ERROR(NULL, "Error reading flash record list from hexfile");
    return NULL;
  }

  return flashRecordList;
}



int _rebootNode(GWEN_MSG_ENDPOINT *epTcp, unsigned int uid, int timeoutInSeconds)
{
  int rv;

  /* send REBOOT_REQUEST message */
  DBG_INFO(NULL, "Sending REBOOT REQUEST message");
  rv=_sendRebootRequest(epTcp, uid);
  if (rv<0) {
    DBG_INFO(NULL, "here (%d)", rv);
    return rv;
  }

  rv=_waitForRebootResponseMessage(epTcp, timeoutInSeconds);
  if (rv!=0) {
    DBG_INFO(NULL, "Bad or no reboot response received (%d).", rv);
    return rv;
  }
  DBG_INFO(NULL, "REBOOT_RESPONSE message received");
  return 0;
}



int _performFlashProcedure(GWEN_MSG_ENDPOINT *epTcp,
                           unsigned int uid,
                           const AQH_FLASHRECORD_LIST *flashRecordList,
                           int pageSize,
                           int timeoutInSeconds)
{
  int rv;
  const AQH_FLASHRECORD *flashRecord;

  fprintf(stdout, "Sending FLASH_START\n");
  rv=_flashStart(epTcp, uid, timeoutInSeconds);
  if (rv<0) {
    DBG_INFO(NULL, "here (%d)", rv);
    return rv;
  }

  /* send FLASH_DATA requests and retrieve FLASH_RSP responses */
  flashRecord=AQH_FlashRecord_List_First(flashRecordList);
  while(flashRecord) {
    DBG_ERROR(NULL, "Sending flash record at %08x", AQH_FlashRecord_GetAddress(flashRecord));
    rv=_sendFlashRecord(epTcp, flashRecord, pageSize, timeoutInSeconds);
    if (rv!=0) {
      DBG_ERROR(NULL, "Error sending flash data (%d)", rv);
      return rv;
    }
    flashRecord=AQH_FlashRecord_List_Next(flashRecord);
  }

  rv=_sendFlashEnd(epTcp, 0);
  if (rv<0) {
    DBG_INFO(NULL, "here (%d)", rv);
    return rv;
  }

  rv=Utils_FlushOutMessageQueue(epTcp, timeoutInSeconds);
  if (rv<0) {
    DBG_INFO(NULL, "here (%d)", rv);
    return rv;
  }

  return 0;
}



int _flashStart(GWEN_MSG_ENDPOINT *epTcp, unsigned int uid, int timeoutInSeconds)
{
  int rv;
  int i;

  for (i=0; i<FLASH_TOOL_MAX_REPEAT; i++) {
    if (i)
      fprintf(stdout, "  sending FLASH_START (retry %d)\n", i);
    /* send FLASH_START message */
    DBG_INFO(NULL, "Sending FLASH START message (%d)", i);
    rv=_sendFlashStart(epTcp, uid);
    if (rv<0) {
      DBG_INFO(NULL, "here (%d)", rv);
      return rv;
    }

    /* wait for response */
    rv=_waitForFlashResponseMessage(epTcp, timeoutInSeconds);
    if (rv!=0) {
      DBG_INFO(NULL, "Bad or no response received (%d).", rv);
      return rv;
    }
    break;
  }
  DBG_INFO(NULL, "FLASH_RESPONSE message received");
  return 0;
}











GWEN_MSG *_waitForFlashReadyMessageForUid(GWEN_MSG_ENDPOINT *epTcp, unsigned int uid, int timeoutInSeconds)
{
  int i;

  for (i=0;i<16;i++) {
    GWEN_MSG *msg;
    unsigned int receivedUid;

    msg=Utils_WaitForSpecificNodeMessage(epTcp, AQH_MSG_TYPE_FLASH_READY, 0, timeoutInSeconds);
    if (msg==NULL) {
      DBG_INFO(NULL, "No FLASH_READY message received.");
      return NULL;
    }

    receivedUid=AQH_FlashReadyMsg_GetUid(msg);
    if (receivedUid==uid) {
      DBG_INFO(NULL, "Received FLASH_READY message for given uid");
      return msg;
    }
    else {
      DBG_INFO(NULL, "Received FLASH_READY message but not for given uid (%08x)", receivedUid);
    }
    GWEN_Msg_free(msg);
  }
  return NULL;
}



int _waitForRebootResponseMessage(GWEN_MSG_ENDPOINT *epTcp, int timeoutInSeconds)
{
  GWEN_MSG *msg;

  msg=Utils_WaitForSpecificNodeMessage(epTcp, AQH_MSG_TYPE_REBOOT_RSP, 0, timeoutInSeconds);
  if (msg==NULL) {
    DBG_INFO(NULL, "No REBOOT_RSP message received.");
    return GWEN_ERROR_IO;
  }
  GWEN_Msg_free(msg);
  return 0;
}



int _waitForFlashResponseMessage(GWEN_MSG_ENDPOINT *epTcp, int timeoutInSeconds)
{
  GWEN_MSG *msg;
  int responseCode;

  msg=Utils_WaitForSpecificNodeMessage(epTcp, AQH_MSG_TYPE_FLASH_RSP, 0, timeoutInSeconds);
  if (msg==NULL) {
    DBG_INFO(NULL, "No FLASH_RSP message received.");
    return GWEN_ERROR_IO;
  }
  responseCode=AQH_FlashResponseMsg_GetCode(msg);
  GWEN_Msg_free(msg);
  return responseCode;
}



int _sendRebootRequest(GWEN_MSG_ENDPOINT *epTcp, unsigned int uid)
{
  GWEN_MSG *msgNode;
  GWEN_MSG *msgOut;

  msgNode=AQH_RebootRequestMsg_new(0, 0xff, AQH_MSG_TYPE_REBOOT_REQ, uid);
  if (msgNode==NULL) {
    DBG_ERROR(NULL, "Error creating message");
    return GWEN_ERROR_GENERIC;
  }

  msgOut=AQH_ForwardIpcMsg_new(AQH_MSGTYPE_IPC_NODES_FORWARD, GWEN_Msg_GetConstBuffer(msgNode), GWEN_Msg_GetBytesInBuffer(msgNode));
  GWEN_MsgEndpoint_AddSendMessage(epTcp, msgOut);
  GWEN_Msg_free(msgNode);
  return 0;
}



int _sendFlashStart(GWEN_MSG_ENDPOINT *epTcp, unsigned int uid)
{
  GWEN_MSG *msgNode;
  GWEN_MSG *msgOut;

  msgNode=AQH_FlashStartMsg_new(0, 0xc1, AQH_MSG_TYPE_FLASH_START, uid, 0, 0);
  if (msgNode==NULL) {
    DBG_ERROR(NULL, "Error creating message");
    return GWEN_ERROR_GENERIC;
  }

  msgOut=AQH_ForwardIpcMsg_new(AQH_MSGTYPE_IPC_NODES_FORWARD, GWEN_Msg_GetConstBuffer(msgNode), GWEN_Msg_GetBytesInBuffer(msgNode));
  GWEN_MsgEndpoint_AddSendMessage(epTcp, msgOut);
  GWEN_Msg_free(msgNode);
  return 0;
}



int _sendFlashEnd(GWEN_MSG_ENDPOINT *epTcp, int reason)
{
  GWEN_MSG *msgNode;
  GWEN_MSG *msgOut;

  msgNode=AQH_FlashEndMsg_new(0, 0xc1, AQH_MSG_TYPE_FLASH_END, reason);
  if (msgNode==NULL) {
    DBG_ERROR(NULL, "Error creating message");
    return GWEN_ERROR_GENERIC;
  }

  msgOut=AQH_ForwardIpcMsg_new(AQH_MSGTYPE_IPC_NODES_FORWARD, GWEN_Msg_GetConstBuffer(msgNode), GWEN_Msg_GetBytesInBuffer(msgNode));
  GWEN_MsgEndpoint_AddSendMessage(epTcp, msgOut);
  GWEN_Msg_free(msgNode);
  return 0;
}



int _sendFlashRecord(GWEN_MSG_ENDPOINT *epTcp,
                     const AQH_FLASHRECORD *flashRecord,
                     uint16_t pageSize,
                     int timeoutInSeconds)
{
  const uint8_t *ptr;
  uint32_t len;
  uint32_t address;

  ptr=AQH_FlashRecord_GetDataPointer(flashRecord);
  len=AQH_FlashRecord_GetDataLength(flashRecord);
  address=AQH_FlashRecord_GetAddress(flashRecord);
  while(ptr && len) {
    uint32_t sendLen;
    int i;

    sendLen=(len>pageSize)?pageSize:len;
    for (i=0;i<FLASH_TOOL_MAX_REPEAT;i++) {
      GWEN_MSG *msgNode;
      GWEN_MSG *msgOut;
      int rv;

#ifdef DEBUG_FLASH
      DBG_ERROR(NULL, "  Sending message: addr=%04x, data len=%d, pagesize=%d", address, sendLen, pageSize);
      GWEN_Text_LogString((const char*) ptr, sendLen, NULL, GWEN_LoggerLevel_Error);
#endif

      if (i)
        fprintf(stdout, "Send data: dest addr=%04x, len=%d bytes (retry %d)\n", address, sendLen, i);
      else
        fprintf(stdout, "Send data: dest addr=%04x, len=%d bytes\n", address, sendLen);

      msgNode=AQH_FlashDataMsg_new(0, 0xc1, AQH_MSG_TYPE_FLASH_DATA, address, ptr, sendLen);
      if (msgNode==NULL) {
        DBG_ERROR(NULL, "Error creating message");
        return GWEN_ERROR_GENERIC;
      }

      msgOut=AQH_ForwardIpcMsg_new(AQH_MSGTYPE_IPC_NODES_FORWARD, GWEN_Msg_GetConstBuffer(msgNode), GWEN_Msg_GetBytesInBuffer(msgNode));
      GWEN_MsgEndpoint_AddSendMessage(epTcp, msgOut);
      GWEN_Msg_free(msgNode);

      rv=_waitForFlashResponseMessage(epTcp, timeoutInSeconds);
      if (rv==0)
        break;
      else {
        DBG_ERROR(NULL, "Negative response to flash data message (%d)", rv);
      }
    } /* for */
    if (i>=FLASH_TOOL_MAX_REPEAT) {
      DBG_ERROR(NULL, "Too many errors (tried %d times), giving up", i);
      /* TODO: send flash end message */
      return GWEN_ERROR_IO;
    }
    ptr+=sendLen;
    address+=sendLen;
    len-=sendLen;
  } /* while */
  return 0;
}






