// The MIT License (MIT)
//
// Copyright (C) 2026 Scott Duensing
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.

// Socket shim  -- rs232-compatible API over TCP sockets
//
// Maps up to 4 "COM ports" to TCP socket file descriptors. Reads use
// MSG_DONTWAIT (non-blocking) to match rs232Read's non-blocking semantics.
// Writes are blocking (loop until all bytes sent) to match rs232Write's
// guarantee of complete delivery.
//
// The shim does NOT close sockets in rs232Close  -- socket lifecycle is
// managed by the proxy's main() function. This avoids double-close bugs
// when secLinkClose calls rs232Close but the proxy still needs the fd.

#include "sockShim.h"
#include "../../libs/kpunch/serial/rs232/rs232.h"

#include <sys/socket.h>
#include <errno.h>
#include <unistd.h>
#include <fcntl.h>
#include <poll.h>


// ========================================================================
// Internal state
// ========================================================================

static int  sFds[RS232_NUM_PORTS] = { -1, -1, -1, -1 };


// ========================================================================
// Public functions (alphabetical)
// ========================================================================

int rs232Close(int com) {
    if (com < 0 || com >= RS232_NUM_PORTS) {
        return RS232_ERR_INVALID_PORT;
    }

    // Socket lifecycle is managed by the caller, not the shim
    return RS232_SUCCESS;
}


// Serial parameters are ignored  -- TCP handles framing, flow control, and
// error correction. We just validate that a socket has been assigned.
int rs232Open(int com, int32_t bps, int dataBits, char parity, int stopBits, int handshake) {
    (void)bps;
    (void)dataBits;
    (void)parity;
    (void)stopBits;
    (void)handshake;

    if (com < 0 || com >= RS232_NUM_PORTS) {
        return RS232_ERR_INVALID_PORT;
    }
    if (sFds[com] < 0) {
        return RS232_ERR_NOT_OPEN;
    }

    return RS232_SUCCESS;
}


// Non-blocking read matching rs232Read semantics:
//   > 0: bytes read
//   0:   no data available (EAGAIN)
//   -1:  error or connection closed (maps to PKT_ERR_DISCONNECTED in pktPoll)
int rs232Read(int com, char *data, int len) {
    if (com < 0 || com >= RS232_NUM_PORTS || sFds[com] < 0) {
        return -1;
    }

    ssize_t n = recv(sFds[com], data, len, MSG_DONTWAIT);
    if (n < 0) {
        if (errno == EAGAIN || errno == EWOULDBLOCK) {
            return 0;
        }
        return -1;
    }
    if (n == 0) {
        // TCP FIN  -- peer closed connection
        return -1;
    }

    return (int)n;
}


// Blocking write: loops until all bytes are sent. MSG_NOSIGNAL prevents
// SIGPIPE on a closed connection (returns EPIPE instead). This matches
// rs232Write's blocking-until-complete behavior.
int rs232Write(int com, const char *data, int len) {
    if (com < 0 || com >= RS232_NUM_PORTS || sFds[com] < 0) {
        return RS232_ERR_NOT_OPEN;
    }

    int sent = 0;
    while (sent < len) {
        ssize_t n = send(sFds[com], data + sent, len - sent, MSG_NOSIGNAL);
        if (n < 0) {
            if (errno == EINTR) {
                continue;
            }

            // Defensive: if the socket is non-blocking for any reason,
            // wait for it to drain rather than returning after a partial
            // write (which would put a truncated frame on the wire).
            if (errno == EAGAIN || errno == EWOULDBLOCK) {
                struct pollfd pfd;
                pfd.fd     = sFds[com];
                pfd.events = POLLOUT;
                poll(&pfd, 1, -1);
                continue;
            }

            return RS232_ERR_NOT_OPEN;
        }
        sent += (int)n;
    }

    return RS232_SUCCESS;
}


void sockShimSetFd(int com, int fd) {
    if (com < 0 || com >= RS232_NUM_PORTS) {
        return;
    }

    sFds[com] = fd;

    // Keep the socket BLOCKING.  rs232Read already does a per-call
    // non-blocking recv (MSG_DONTWAIT), so it needs no socket-wide flag;
    // a non-blocking socket would instead make rs232Write's send() return
    // EAGAIN when the TCP buffer fills, truncating an HDLC frame (the
    // caller in packet.c ignores the return).  Blocking matches the real
    // rs232Write's busy-wait-until-complete contract.
    int flags = fcntl(fd, F_GETFL, 0);

    if (flags >= 0) {
        fcntl(fd, F_SETFL, flags & ~O_NONBLOCK);
    }
}
