#ifndef pmacAsynIPPort_H
#define pmacAsynIPPort_H

#include <macros.h>
#include <shareLib.h>

#include "asynPortDriver.h"

#include <cstddef>
#include <cstdint>
#include <string>
#include <vector>

#include <osiSock.h>

/*
Value is chosen arbitrarily, it just needs to be unique
*/
#define FLUSH_HARDWARE 1

constexpr size_t ETHERNET_DATA_SIZE = 1492;

/* PMAC ethernet command structure */
#pragma pack(push, 1)
struct EthernetCmd {
    uint8_t RequestType;
    uint8_t Request;
    uint16_t wValue;
    uint16_t wIndex;
    uint16_t wLength; /* length of bData */
    uint8_t bData[ETHERNET_DATA_SIZE];
};
#pragma pack(pop)

constexpr size_t ETHERNET_CMD_HEADER = sizeof(EthernetCmd) - ETHERNET_DATA_SIZE;

/**
 * @brief asyn driver speaking the PMAC ethernet protocol directly over a TCP
 * socket.
 *
 * This driver replaces the generic drvAsynIPPort driver for the communication
 * with a Turbo PMAC controller. It handles the socket layer itself and builds
 * and parses the PMAC ethernet packets in its asynOctet methods. The
 * asynCommon connect/disconnect handling and the asynInt32 interface (used for
 * FLUSH_HARDWARE) are implemented here as well.
 */
class HIDDEN pmacAsynIPPort : public asynPortDriver {
  public:
    /**
     * @brief Construct a new pmacAsynIPPort object
     *
     * The port is registered with asynManager in the asynPortDriver base class
     * constructor and can therefore safely be "leaked" here. See
     * turboPmacCreateAxis in turboPmacAxis.cpp for a similar pattern.
     *
     * @param portName The Asyn Port name string.
     * @param hostInfo The hostname or IP address followed by the IP port (eg.
     * 172.23.243.156:1025)
     */
    pmacAsynIPPort(const char *portName, const char *hostInfo);

    virtual ~pmacAsynIPPort();

    // asynCommon interface overrides
    virtual asynStatus connect(asynUser *pasynUser) override;
    virtual asynStatus disconnect(asynUser *pasynUser) override;

    // asynOctet interface overrides
    virtual asynStatus writeOctet(asynUser *pasynUser, const char *data,
                                  size_t numchars,
                                  size_t *nbytesTransfered) override;
    virtual asynStatus readOctet(asynUser *pasynUser, char *data,
                                 size_t maxchars, size_t *nbytesTransfered,
                                 int *eomReason) override;
    virtual asynStatus flushOctet(asynUser *pasynUser) override;

    // asynInt32 interface overrides
    virtual asynStatus writeInt32(asynUser *pasynUser,
                                  epicsInt32 value) override;

  private:
    /**
     * @brief Parse a "<host>:<port>" string into IPHostName_ and port_.
     *
     * @throws std::runtime_error if hostInfo is not of the form
     * "<host>:<port>".
     */
    void parseHostInfo(const char *hostInfo);

    /**
     * @brief Open a TCP connection to the PMAC.
     *
     * The host name lookup is delayed until this point, so that a device which
     * gets a new IP address via DHCP is found again on the next reconnect
     * attempt.
     */
    asynStatus connectIt(asynUser *pasynUser);

    /**
     * @brief Close the TCP connection and notify asynManager.
     *
     * This marks the port as disconnected, so that asynManager's autoConnect
     * logic retries to establish a connection.
     */
    void closeConnection(asynUser *pasynUser, const char *why);

    /**
     * @brief Raw read from the TCP socket.
     *
     * Polls the socket for up to pasynUser->timeout and returns any received
     * data. A timeout with no data returns asynTimeout and does not close the
     * connection.
     */
    asynStatus socketRead(asynUser *pasynUser, char *data, size_t maxchars,
                          size_t *nbytesRead);

    /**
     * @brief Raw write to the TCP socket.
     *
     * If nbytesTransferred is not null, it receives the number of bytes that
     * were sent to the socket.
     */
    asynStatus socketWrite(asynUser *pasynUser, const char *data,
                           size_t numchars, size_t *nbytesWritten);

    /**
     * @brief Poll the socket fd, retrying while poll() is interrupted by
     * EINTR.
     *
     * @return asynSuccess with *pollstatus holding poll()'s return value, or
     * asynError if poll() failed for a different reason.
     */
    asynStatus pollSocket(asynUser *pasynUser, struct pollfd *pollfd,
                          int pollmsec, int *pollstatus);

    /**
     * @brief Fill in a PMAC ethernet command header and send it over the
     * socket.
     *
     * Only the 8 byte command header is sent. Commands that carry a payload
     * (e.g. GETRESPONSE in writeOctet) are sent directly via socketWrite.
     */
    asynStatus sendPmacCommand(asynUser *pasynUser, EthernetCmd &cmd,
                               uint8_t requestType, uint8_t request,
                               uint16_t wValue, uint16_t wLength,
                               size_t *nbytesTransfered);

    /**
     * @brief Read response data from the PMAC into the internal input buffer
     *
     * If there is no data in the socket buffer then a GETBUFFER command is
     * issued to get any outstanding data still on the PMAC.
     */
    asynStatus readResponse(asynUser *pasynUser, size_t maxchars,
                            size_t *nbytesTransfered, int *eomReason);

    /**
     * @brief Send a ReadReady command to the PMAC to discover if there is any
     * data to read from it.
     *
     * @return 0 - no data available, 1 - data available
     */
    int pmacReadReady(asynUser *pasynUser);

    /**
     * @brief Send a Flush command to the PMAC and wait for confirmation.
     *
     * @return 0 - failed, 1 - success
     */
    int pmacFlush(asynUser *pasynUser);

    /**
     * @brief Send a GETBUFFER command to the PMAC.
     */
    asynStatus sendPmacGetBuffer(asynUser *pasynUser, size_t maxchars);

    // Host name and TCP port of the PMAC
    std::string IPHostName_;
    unsigned short port_ = 0;

    // TCP socket used for the connection
    SOCKET fd_ = INVALID_SOCKET;

    // Command buffers used for the PMAC ethernet protocol
    EthernetCmd poutCmd_{};
    EthernetCmd pinCmd_{};

    // Buffer for incoming response data
    std::vector<char> inBuf_;
    size_t inBufHead_ = 0;
    size_t inBufTail_ = 0;
};

/**
 * @brief Initialize a special asyn IP Port for PMAC controllers.
 *
 * Wrapper which creates a pmacAsynIPPort object and interposes the EOS
 * handling layer above it.
 *
 * @param portName The Asyn Port name string.
 * @param hostInfo The hostname or IP address followed by the IP port (eg.
 * 172.23.243.156:1025)
 * @return status
 */
int pmacAsynIPPortConfigure(const char *portName, const char *hostInfo);

#endif /* pmacAsynIPPort_H */
