//
//  File:   hzUdpClient.cpp
//
//  Legal Notice:   This file is part of the HadronZoo C++ Class Library. Copyright 2025 HadronZoo Project (http://www.hadronzoo.com)
//
//  The HadronZoo C++ Class Library is free software: You can redistribute it, and/or modify it under the terms of the GNU Lesser General Public License, as published by the Free
//  Software Foundation, either version 3 of the License, or any later version.
//
//  The HadronZoo C++ Class Library 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 Lesser General Public License for more details.
//
//  You should have received a copy of the GNU Lesser General Public License along with the HadronZoo C++ Class Library. If not, see http://www.gnu.org/licenses.
//
//
//  Implimentation of the generic UDP client class, hzUdpClient.
//
#include <iostream>
#include <stdarg.h>
#include <unistd.h>
#include <netdb.h>
#include <sys/socket.h>
#include "hzProcess.h"
#include "hzUdpClient.h"
hzEcode hzUdpClient::Connect    (const hzString& Hostname, uint32_t nPort, bool bLocal)
{
    //  There is of course, no such thing as a UDP connection in the same way there are TCP connections. However the host must still be looked up and a socket
    //  obtained before one can send and receive data so in terms of function call sequence, the two forms of communication are conceptually similar.
    //
    //  It is the role of this function to do the host lookup and obtain the datagram socket.
    //
    //  Arguments:  1)  Hostname    The server name or IP address (as hzString)
    //              2)  nPort       The port number
    //              3)  bLocal      Optional local flag (if host is this machine)
    //
    //  Returns:    E_ARGUMENT      If either the hostname or port is not specified.
    //              E_DNS_NOHOST    If the domain does not exist
    //              E_DNS_NODATA    If the domain exists but not a service
    //              E_DNS_FAILED    If the domain has invalid settings
    //              E_DNS_RETRY     If the DNS is busy
    //              E_NOSOCKET      If the socket could not be created or timeouts set
    //              E_OK            If host established and socket obtained
    _hzfunc("hzUdpClient::Connect") ;
    timeval tv ;    //  Socket timer
    tv.tv_sec = 20 ;
    tv.tv_usec = 0 ;
    /*
    **  Initialize
    */
    m_Hostname = Hostname ;
    m_nPort = nPort ;
    if (!Hostname)
    {
        hzerr(E_ARGUMENT, "Hostname not set") ;
        return E_ARGUMENT ;
    }
    m_pHost = gethostbyname(*m_Hostname) ;
    if (!m_pHost)
    {
        if (h_errno == TRY_AGAIN)       return E_DNS_RETRY ;
        if (h_errno == HOST_NOT_FOUND)  return E_DNS_NOHOST ;
        if (h_errno == NO_RECOVERY)     return E_DNS_FAILED ;
        if (h_errno == NO_DATA || h_errno == NO_ADDRESS)
            return E_DNS_NODATA ;
        hzerr(E_DNS_FAILED, "Could not resolve hostname") ;
        return E_DNS_FAILED ;
    }
    if (m_nPort == 0)
    {
        hzerr(E_ARGUMENT, "Port not set") ;
        return E_ARGUMENT ;
    }
    m_SvrLen = sizeof(m_SvrAddr) ;
    /*
    **  Set up socket
    */
    memset(&m_SvrAddr, 0, sizeof(m_SvrAddr)) ;
    m_SvrAddr.sin_family = AF_INET ;
    memcpy(&m_SvrAddr.sin_addr, m_pHost->h_addr, m_pHost->h_length) ;
    m_SvrAddr.sin_port = htons(m_nPort) ;
    if ((m_nSock = socket(AF_INET, SOCK_DGRAM, 0)) < 0)
    {
        hzerr(E_NOSOCKET, "Could not create client socket") ;
        return E_NOSOCKET ;
    }
    if (setsockopt(m_nSock, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv)) < 0)
    {
        hzerr(E_NOSOCKET, "Could not set send timeout on UDP client socket") ;
        return E_NOSOCKET ;
    }
    if (setsockopt(m_nSock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)) < 0)
    {
        hzerr(E_NOSOCKET, "Could not set recv timeout on UDP client socket") ;
        return E_NOSOCKET ;
    }
    return E_OK ;
}
hzEcode hzUdpClient::SendPkt    (hzPacket* pData, uint32_t nLen)
{
    //  Purpose:    Write buffer content to a socket
    //
    //  Arguments:  1)  pData   The buffer (void*)
    //              2)  nLen    Number of bytes to send
    //
    //  Returns:    E_NOSOCKET  If no socket has been set by Connect()
    //              E_ARGUMENT  If the data is not provided
    //              E_RANGE     If the number of bytes to send exceeds packet size (1460 bytes)
    //              E_SENDFAIL  If the send operation fails. In this event the connection is closed.
    //              E_OK        If the operation is successfull.
    _hzfunc("hzUdpClient::Send(void*,uint32_t)") ;
    if (m_nSock == 0)
        return E_NOSOCKET ;
    if (!pData)
    {
        hzerr(E_ARGUMENT, "Nothing to send") ;
        return E_ARGUMENT ;
    }
    if (nLen > HZ_MAXPACKET)
    {
        hzerr(E_RANGE, "Length of message must be between 1 and 1460 bytes") ;
        return E_RANGE ;
    }
    //  send a message
    if (sendto(m_nSock, pData->m_data, nLen, 0, (struct sockaddr*) &m_SvrAddr, m_SvrLen) < 0)
    {
        close(m_nSock) ;
        m_nSock = 0 ;
        hzerr(E_SENDFAIL, "Could not send to host (%s) on port %d", *m_Hostname, m_nPort) ;
        return E_SENDFAIL ;
    }
    threadLog("Client sock %d sends msg of %d bytes [%s]\n", m_nSock, nLen, pData->m_data) ;
    return E_OK ;
}
hzEcode hzUdpClient::SendChain  (hzChain& C)
{
    //  Write supplied chain content to a socket
    //
    //  Arguments:  1)  C   The chain to send. No size indicator is needed as whole chain is sent.
    //
    //  Returns:    E_NOSOCKET  If the connection is not open
    //              E_NODATA    If the supplied chain contains no data
    //              E_SENDFAIL  If the send operation fails. In this event the connection is closed.
    //              E_OK        If the operation is successfull.
    _hzfunc("hzUdpClient::Send(hzChain&)") ;
    chIter      ci ;        //  To iterate input chain
    char*       i ;         //  To populate buffer
    uint32_t    nSend ;     //  Bytes in buffer to send
    uint32_t    nSofar ;    //  Bytes sent so far
    if (m_nSock == 0)
        return E_NOSOCKET ;
    if (!C.Size())
        return E_NODATA ;
    //  Init
    ci = C ;
    nSofar = 0 ;
    //  Read from chain to populate rest of buffer with start of message
    for (nSend = 0, i = m_pack.m_data ; !ci.eof() && nSend < HZ_MAXPACKET ; *i = *ci, i++, nSend++, ci++) ;
    nSofar = nSend ;
    //  Send buffer content to socket
    if (sendto(m_nSock, m_pack.m_data, nSend, 0, (struct sockaddr*) &m_SvrAddr, m_SvrLen) < 0)
    {
        close(m_nSock) ;
        m_nSock = 0 ;
        hzerr(E_SENDFAIL, "Could not send to host (%s) on port %d", *m_Hostname, m_nPort) ;
        return E_SENDFAIL ;
    }
    //  Repeat read and send steps for rest of message
    for (; nSofar < C.Size() ;)
    {
        for (nSend = 0, i = m_pack.m_data ; !ci.eof() && nSend < HZ_MAXPACKET ; *i = *ci, nSend++, i++, ci++) ;
        nSofar += nSend ;
        if (!nSend)
            break ;
        if (sendto(m_nSock, m_pack.m_data, nSend, 0, (struct sockaddr*) &m_SvrAddr, m_SvrLen) < 0)
        {
            close(m_nSock) ;
            m_nSock = 0 ;
            hzerr(E_SENDFAIL, "Could not send to host (%s) on port %d", *m_Hostname, m_nPort) ;
            return E_SENDFAIL ;
        }
    }
    return E_OK ;
}
hzEcode hzUdpClient::RecvPkt    (hzPacket* pData, uint32_t& nRecv)
{
    //  Read a packet from the socket into the client buffer.
    //
    //  Arguments:  1)  pData   The packet recepticle
    //              2)  nRecv   Reference to number of bytes received
    //
    //  Returns:    E_ARGUMENT  If the packet recipticle is not supplied
    //              E_NOSOCKET  If the connection has been closed
    //              E_NODATA    If no data was recieved
    //              E_RECVFAIL  If the socket read operation fails
    //              E_OK        If operation successfull
    _hzfunc("hzUdpClient::Recv(buf,recv,max)") ;
    if (!pData)
    {
        hzerr(E_ARGUMENT, "No IP recepticle supplied") ;
        return E_ARGUMENT ;
    }
    if (m_nSock == 0)
    {
        hzerr(E_NOSOCKET, "Client has no connection") ;
        return E_NOSOCKET ;
    }
    //  Get response
    //nRecv = recvfrom(m_nSock, pData->m_data, HZ_MAXPACKET, 0, (struct sockaddr*) &m_SvrAddr, &m_SvrLen) ;
    nRecv = recvfrom(m_nSock, pData->m_data, HZ_MAXPACKET, 0, (SOCKADDR*) &m_SvrAddr, &m_SvrLen) ;
    if (nRecv == 0)
        return E_NODATA ;
    if (nRecv < 0)
    {
        close(m_nSock) ;
        m_nSock = 0 ;
        hzerr(E_RECVFAIL, "Could not recv from server (%s) on port %d", *m_Hostname, m_nPort) ;
        return E_RECVFAIL ;
    }
    pData->m_data[nRecv] = 0 ;
    threadLog("Client sock %d recv %d bytes\n", m_nSock, nRecv) ;
    return E_OK ;
}
hzEcode hzUdpClient::RecvChain  (hzChain& C)
{
    //  Reads one or more packets from the socket into the supplied chain until a zero length read occurs.
    //
    //  Argument:   C   The chain to populate
    //
    //  Returns:    E_NOSOCKET  If the connection has been closed
    //              E_RECVFAIL  If the socket read operation fails
    //              E_OK        If operation successfull
    _hzfunc("hzUdpClient::Recv(hzChain&)") ;
    uint32_t    nRecv ;     //  Bytes recieved in IP packet
    //  CHECK
    C.Clear() ;
    if (m_nSock == 0)
    {
        hzerr(E_NOSOCKET, "Client has no connection") ;
        return E_NOSOCKET ;
    }
    if ((nRecv = recvfrom(m_nSock, m_pack.m_data, HZ_MAXPACKET, 0, (struct sockaddr*) &m_SvrAddr, &m_SvrLen)) < 0)
    {
        close(m_nSock) ;
        m_nSock = 0 ;
        hzerr(E_RECVFAIL, "Could not recv from server (%s) on port %d", *m_Hostname, m_nPort) ;
        return E_RECVFAIL ;
    }
    C.Append(m_pack.m_data, nRecv) ;
    for (;;)
    {
        nRecv = recvfrom(m_nSock, m_pack.m_data, HZ_MAXPACKET, 0, (struct sockaddr*) &m_SvrAddr, &m_SvrLen) ;
        if (nRecv < 0)
        {
            close(m_nSock) ;
            m_nSock = 0 ;
            hzerr(E_RECVFAIL, "Could not recv from server (%s) on port %d", *m_Hostname, m_nPort) ;
            return E_RECVFAIL ;
        }
        if (nRecv == 0)
            break ;
        C.Append(m_pack.m_data, nRecv) ;
    }
    return E_OK ;
}