//
// Copyright 2024 Staysail Systems, Inc. <info@staysail.tech>
// Copyright 2018 Capitar IT Group BV <info@capitar.com>
// Copyright 2018 Devolutions <info@devolutions.net>
//
// This software is supplied under the terms of the MIT License, a
// copy of which should be located in the distribution where this
// file was obtained (LICENSE.txt).  A copy of the license may also be
// found online at https://opensource.org/licenses/MIT.
//

#ifndef CORE_PLATFORM_H
#define CORE_PLATFORM_H

// We require some standard C header files.  The only one of these that might
// be problematic is <stdint.h>, which is required for C99.  Older versions
// of the Windows compilers might not have this.  However, latest versions of
// MS Studio have a functional <stdint.h>.  If this impacts you, just upgrade
// your tool chain.
#include <stdarg.h>
#include <stddef.h>
#include <stdint.h>

// These are the APIs that a platform must implement to support nng.

// A word about fork-safety: This library is *NOT* fork safe, in that
// functions may not be called in the child process without an intervening
// exec().  The library attempts to detect this situation, and crashes the
// process with an error message if it encounters it.  (See nn_platform_init
// below.)
//
// Additionally, some file descriptors may leak across fork even to
// child processes.  We make every reasonable effort to ensure that this
// does not occur, but on some platforms there are unavoidable race
// conditions between file creation and marking the file close-on-exec.
//
// Forkers should use posix_spawn() if possible, and as much as possible
// arrange for file close on exec by posix_spawn, or close the descriptors
// they do not need in the child.  (Note that posix_spawn() does *NOT*
// arrange for pthread_atfork() handlers to be called on some platforms.)

//
// Debugging Support
//

// nni_plat_abort crashes the system; it should do whatever is appropriate
// for abnormal programs on the platform, such as calling abort().
extern void nni_plat_abort(void);

// nni_plat_println is used to emit debug messages.  Typically this is used
// during core debugging, or to emit panic messages.  Message content will
// not contain newlines, but the output will add them.
extern void nni_plat_println(const char *);

// nni_plat_printf is like printf.  It should conform to C99 standard printf,
// but is a function to allow platform ports to redirect.  It should go to
// the same place that nni_plat_println does.
extern void nni_plat_printf(const char *, ...);

// nni_plat_strerror allows the platform to use additional error messages
// for additional error codes.  The err code passed in should be the
// equivalent of errno or GetLastError, without the NNG_ESYSERR component.
// The platform should make sure that the returned value will be valid
// after the call returns.  (If necessary, thread-local storage can be
// used.)
extern const char *nni_plat_strerror(int);

//
// Memory Management
//

// nni_alloc allocates memory.  In most cases this can just be malloc().
// However, you may provide a different allocator, for example it is
// possible to use a slab allocator or somesuch.  It is permissible for this
// to return NULL if memory cannot be allocated.
extern void *nni_alloc(size_t);

// nni_zalloc is just like nni_alloc, but ensures that memory is
// initialized to zero.  It is a separate function because some platforms
// can use a more efficient zero-based allocation.
extern void *nni_zalloc(size_t);

// nni_free frees memory allocated with nni_alloc or nni_zalloc. It takes
// a size because some allocators do not track size, or can operate more
// efficiently if the size is provided with the free call.  Examples of this
// are slab allocators like this found in Solaris/illumos (see libumem).
// This routine does nothing if supplied with a NULL pointer and zero size.
// Most implementations can just call free() here.
extern void nni_free(void *, size_t);

typedef struct nni_plat_mtx    nni_plat_mtx;
typedef struct nni_plat_rwlock nni_plat_rwlock;
typedef struct nni_plat_cv     nni_plat_cv;
typedef struct nni_plat_thr    nni_plat_thr;
typedef struct nni_rwlock      nni_rwlock;

//
// Threading & Synchronization Support
//

// nni_plat_mtx_init initializes a mutex structure.  An initialized mutex must
// be distinguishable from zeroed memory.
extern void nni_plat_mtx_init(nni_plat_mtx *);

// nni_plat_mtx_fini destroys the mutex and releases any resources allocated
// for it's use.  If the mutex is zeroed memory, this should do nothing.
extern void nni_plat_mtx_fini(nni_plat_mtx *);

// nni_plat_mtx_lock locks the mutex.  This is not recursive -- a mutex can
// only be entered once.
extern void nni_plat_mtx_lock(nni_plat_mtx *);

// nni_plat_mtx_unlock unlocks the mutex.  This can only be performed by the
// thread that owned the mutex.
extern void nni_plat_mtx_unlock(nni_plat_mtx *);

// read/write locks - these work like mutexes except that multiple readers
// can acquire the lock.  These are not safe for recursive use, and it is
// unspecified whether any measures are provided to prevent starvation.
extern void nni_rwlock_init(nni_rwlock *);
extern void nni_rwlock_fini(nni_rwlock *);
extern void nni_rwlock_rdlock(nni_rwlock *);
extern void nni_rwlock_wrlock(nni_rwlock *);
extern void nni_rwlock_unlock(nni_rwlock *);

// nni_plat_cv_init initializes a condition variable.  We require a mutex be
// supplied with it, and that mutex must always be held when performing any
// operations on the condition variable (other than fini.)  As with mutexes, an
// initialized mutex should be distinguishable from zeroed memory.
extern void nni_plat_cv_init(nni_plat_cv *, nni_plat_mtx *);

// nni_plat_cv_fini releases all resources associated with condition variable.
// If the cv points to just zeroed memory (was never initialized), it does
// nothing.
extern void nni_plat_cv_fini(nni_plat_cv *);

// nni_plat_cv_wake wakes all waiters on the condition.  This should be
// called with the lock held.
extern void nni_plat_cv_wake(nni_plat_cv *);

// nni_plat_cv_wake1 wakes only a single waiter.  Use with caution
// to avoid losing the wakeup when multiple waiters may be present.
extern void nni_plat_cv_wake1(nni_plat_cv *);

// nni_plat_cv_wait waits for a wake up on the condition variable.  The
// associated lock is atomically released and reacquired upon wake up.
// Callers can be spuriously woken.  The associated lock must be held.
extern void nni_plat_cv_wait(nni_plat_cv *);

// nni_plat_cv_until waits for a wakeup on the condition variable, or
// until the system time reaches the specified absolute time.  (It is an
// absolute form of nni_cond_timedwait.)  Early wakeups are possible, so
// check the condition.  It will return either NNG_ETIMEDOUT, or 0.
extern int nni_plat_cv_until(nni_plat_cv *, nni_time);

// nni_plat_thr_init creates a thread that runs the given function. The
// thread receives a single argument.  The thread starts execution
// immediately.
extern int nni_plat_thr_init(nni_plat_thr *, void (*)(void *), void *);

// nni_plat_thr_fini waits for the thread to exit, and then releases any
// resources associated with the thread.  After this returns, it
// is an error to reference the thread in any further way.
extern void nni_plat_thr_fini(nni_plat_thr *);

// nni_plat_thr_is_self returns true if the caller is the thread
// identified, and false otherwise.  (This allows some deadlock
// prevention in callbacks, for example.)
extern bool nni_plat_thr_is_self(nni_plat_thr *);

// nni_plat_thr_set_name is used to set the thread name, which
// should be a short ASCII string.  It may or may not be supported --
// this is intended to facilitate debugging.
extern void nni_plat_thr_set_name(nni_plat_thr *, const char *);

//
// Atomics support.  This will evolve over time.
//

// nni_atomic_flag supports only test-and-set and reset operations.
// This can be implemented without locks on any reasonable system, and
// it corresponds to C11 atomic flag.
typedef struct nni_atomic_flag nni_atomic_flag;

extern bool nni_atomic_flag_test_and_set(nni_atomic_flag *);
extern void nni_atomic_flag_reset(nni_atomic_flag *);

// nni_atomic_bool is for boolean flags that need to be checked without
// changing their value.  This might require a lock on some systems.
typedef struct nni_atomic_bool nni_atomic_bool;

extern void nni_atomic_init_bool(nni_atomic_bool *);
extern void nni_atomic_set_bool(nni_atomic_bool *, bool);
extern bool nni_atomic_get_bool(nni_atomic_bool *);
extern bool nni_atomic_swap_bool(nni_atomic_bool *, bool);

typedef struct nni_atomic_u64 nni_atomic_u64;

extern void     nni_atomic_init64(nni_atomic_u64 *);
extern void     nni_atomic_add64(nni_atomic_u64 *, uint64_t);
extern void     nni_atomic_sub64(nni_atomic_u64 *, uint64_t);
extern uint64_t nni_atomic_get64(nni_atomic_u64 *);
extern void     nni_atomic_set64(nni_atomic_u64 *, uint64_t);
extern uint64_t nni_atomic_swap64(nni_atomic_u64 *, uint64_t);
extern uint64_t nni_atomic_dec64_nv(nni_atomic_u64 *);
extern void     nni_atomic_inc64(nni_atomic_u64 *);

// nni_atomic_cas64 is a compare and swap.  The second argument is the
// value to compare against, and the third is the new value. Returns
// true if the value was set.
extern bool nni_atomic_cas64(nni_atomic_u64 *, uint64_t, uint64_t);

// In a lot of circumstances, we want a simple atomic reference count,
// or atomic tunable values for integers like queue lengths or TTLs.
// These native integer forms should be preferred over the 64 bit versions
// unless larger bit sizes are truly needed.  They will be more efficient
// on many platforms.
typedef struct nni_atomic_int nni_atomic_int;

extern void nni_atomic_init(nni_atomic_int *);
extern void nni_atomic_add(nni_atomic_int *, int);
extern void nni_atomic_sub(nni_atomic_int *, int);
extern int  nni_atomic_get(nni_atomic_int *);
extern void nni_atomic_set(nni_atomic_int *, int);
extern int  nni_atomic_swap(nni_atomic_int *, int);
extern int  nni_atomic_dec_nv(nni_atomic_int *);
extern void nni_atomic_dec(nni_atomic_int *);
extern void nni_atomic_inc(nni_atomic_int *);

// nni_atomic_cas is a compare and swap.  The second argument is the
// value to compare against, and the third is the new value. Returns
// true if the value was set.
extern bool nni_atomic_cas(nni_atomic_int *, int, int);

// atomic pointers.  We only support a few operations.
typedef struct nni_atomic_ptr nni_atomic_ptr;
extern void                   nni_atomic_set_ptr(nni_atomic_ptr *, void *);
extern void                  *nni_atomic_get_ptr(nni_atomic_ptr *);

//
// Clock Support
//

// nn_clock returns a number of milliseconds since some arbitrary time
// in the past.  The values returned by nni_clock must use the same base
// as the times used in nni_plat_cond_until.  The nni_clock() must
// return values > 0, and must return values smaller than 2^63.  (We could
// relax this last constraint, but there is no reason to, and leaves us the
// option of using negative values for other purposes in the future.)
extern nni_time nni_clock(void);

// Get the real time, in seconds and nanoseconds
extern int nni_time_get(uint64_t *seconds, uint32_t *nanoseconds);

// nni_msleep sleeps for the specified number of milliseconds (at least).
extern void nni_msleep(nni_duration);

//
// Entropy Support
//

// nni_random returns a cryptographically secure random uint32.
uint32_t nni_random(void);

// nni_plat_init is called to allow the platform the chance to
// do any necessary initialization.  This routine MUST be idempotent,
// and thread-safe, and will be called before any other API calls, and
// may be called at any point thereafter.  It is permitted to return
// an error if some critical failure initializing the platform occurs,
// but once this succeeds, all future calls must succeed as well, unless
// nni_plat_fini has been called.
//
// The function argument should be called if the platform has not initialized
// (i.e. exactly once), and its result passed back to the caller.  If it
// does not return 0 (success), then it may be called again to try to
// initialize the platform again at a later date.
extern int nni_plat_init(int (*)(void));

// nni_plat_fini is called to clean up resources.  It is intended to
// be called as the last thing executed in the library, and no other functions
// will be called until nni_platform_init is called.
extern void nni_plat_fini(void);

// nni_plat_ncpu returns the number of logical CPUs on the system.  This is
// used to scale the number of independent threads started.
extern int nni_plat_ncpu(void);

//
// TCP Support.
//
typedef struct nni_tcp_conn     nni_tcp_conn;
typedef struct nni_tcp_dialer   nni_tcp_dialer;
typedef struct nni_tcp_listener nni_tcp_listener;

// nni_tcp_dialer_init creates a new dialer object.
extern int nni_tcp_dialer_init(nni_tcp_dialer **);

// nni_tcp_dialer_fini finalizes the dialer, closing it and freeing
// all resources.
extern void nni_tcp_dialer_fini(nni_tcp_dialer *);

// nni_tcp_dialer_close closes the dialer.
// Further operations on it should return NNG_ECLOSED.
// Any in-progress connection will be aborted.
extern void nni_tcp_dialer_close(nni_tcp_dialer *);

// nni_tcp_dial attempts to create an outgoing connection,
// asynchronously, to the address in the aio. On success, the first (and only)
// output will be an nni_tcp_conn * associated with the remote server.
extern void nni_tcp_dial(nni_tcp_dialer *, const nng_sockaddr *, nni_aio *);

// nni_tcp_dialer_set sets an option on the dialer.
extern int nni_tcp_dialer_set(
    nni_tcp_dialer *, const char *, const void *, size_t, nni_type);

// nni_tcp_dialer_get gets an option on the dialer.
extern int nni_tcp_dialer_get(
    nni_tcp_dialer *, const char *, void *, size_t *, nni_type);

// nni_tcp_listener_init creates a new listener object, unbound.
extern int nni_tcp_listener_init(nni_tcp_listener **);

// nni_tcp_listener_fini frees the listener and all associated resources.
// It implicitly closes the listener as well.
extern void nni_tcp_listener_fini(nni_tcp_listener *);

// nni_tcp_listener_close closes the listener.  This will unbind
// any bound socket, and further operations will result in NNG_ECLOSED.
extern void nni_tcp_listener_close(nni_tcp_listener *);

// nni_tcp_listener_listen creates the socket in listening mode, bound
// to the specified address.
extern int nni_tcp_listener_listen(nni_tcp_listener *, const nni_sockaddr *);

// nni_tcp_listener_accept accepts in incoming connect, asynchronously.
// On success, the first (and only) output will be an nni_tcp_conn *
// associated with the remote peer.
extern void nni_tcp_listener_accept(nni_tcp_listener *, nni_aio *);

// nni_tcp_listener_set sets an option on the listener.
extern int nni_tcp_listener_set(
    nni_tcp_listener *, const char *, const void *, size_t, nni_type);

// nni_tcp_listener_get gets an option from the listener.  The most common
// use for this is to retrieve the setting of the NNG_OPT_TCP_LOCADDR
// address after binding to wild card port (0).
extern int nni_tcp_listener_get(
    nni_tcp_listener *, const char *, void *, size_t *, nni_type);

// nni_resolv_ip resolves a DNS host and service name asynchronously.
// The family should be one of NNG_AF_INET, NNG_AF_INET6, or NNG_AF_UNSPEC.
// The first two constrain the name to those families, while the third will
// return names of either family.  The passive flag indicates that the
// name will be used for bind(), otherwise the name will be used with
// connect().  The host part may be NULL only if passive is true.
// Symbolic service names will be looked up assuming SOCK_STREAM, so
// they may not work with UDP.
extern void nni_resolv_ip(
    const char *, const char *, int, bool, nng_sockaddr *sa, nni_aio *);

// nni_parse_ip parses an IP address, without a port.
extern int nni_parse_ip(const char *, nng_sockaddr *);

// nni_parse_ip_port parses an IP address with an optional port appended.
extern int nni_parse_ip_port(const char *, nng_sockaddr *);

//
// IPC (UNIX Domain Sockets & Named Pipes) Support.
//

typedef struct nni_ipc_conn     nni_ipc_conn;
typedef struct nni_ipc_dialer   nni_ipc_dialer;
typedef struct nni_ipc_listener nni_ipc_listener;

// IPC is so different from platform to platform.  The following should
// be implemented.  If IPC isn't supported, all of these functions should
// be stubs that just return NNG_ENOTSUP.
extern int nni_ipc_dialer_alloc(nng_stream_dialer **, const nng_url *);
extern int nni_ipc_listener_alloc(nng_stream_listener **, const nng_url *);

//
// UDP support. UDP is not connection oriented, and only has the notion
// of being bound, sendto, and recvfrom.  (It is possible to set up a
// connect call that semantically acts as a filter on recvfrom, but we
// don't use that.)  Outbound packets will include the destination address
// in the AIO, and inbound packets include the source address in the AIO.
// For now we don't have more sophisticated options like setting the TTL.
//
typedef struct nni_plat_udp nni_plat_udp;

// nni_plat_udp_open initializes a UDP socket, binding to the local
// address specified in the AIO.  The remote address is
// not used.  The resulting nni_plat_udp structure is returned in the
// aio's a_pipe.
extern int nni_plat_udp_open(nni_plat_udp **, nni_sockaddr *);

// nni_plat_udp_close closes the underlying UDP socket.
extern void nni_plat_udp_close(nni_plat_udp *);

// nni_plat_udp_send sends the data in the aio to the the
// destination specified in the nni_aio.  The iovs are the
// UDP payload.
extern void nni_plat_udp_send(nni_plat_udp *, nni_aio *);

// nni_plat_udp_pipe_recv recvs a message, storing it in the iovs
// from the UDP payload.  If the UDP payload will not fit, then
// NNG_EMSGSIZE results.
extern void nni_plat_udp_recv(nni_plat_udp *, nni_aio *);

//
// Notification Pipe Pairs
//

// nni_plat_pipe creates a pair of linked file descriptors that are
// suitable for notification via SENDFD/RECVFD.  These are platform
// specific and exposed to applications for integration into event loops.
// The first pipe is written to by nng to notify, and the second pipe is
// generally read from to clear the event.   The implementation is not
// obliged to provide two pipes -- for example eventfd can be used with
// just a single file descriptor.  In such a case the implementation may
// just provide the same value twice.
extern int nni_plat_pipe_open(int *, int *);

// nni_plat_pipe_raise pushes a notification to the pipe.  Usually this
// will just be a non-blocking attempt to write a single byte.  It may
// however use any other underlying system call that is appropriate.
extern void nni_plat_pipe_raise(int);

// nni_plat_pipe_clear clears all notifications from the pipe.  Usually this
// will just be a non-blocking read.  (The call should attempt to read
// all data on a pipe, for example.)
extern void nni_plat_pipe_clear(int);

// nni_plat_pipe_close closes both pipes that were provided by the open
// routine.
extern void nni_plat_pipe_close(int, int);

extern int nni_plat_udp_sockname(nni_plat_udp *, nni_sockaddr *);

// nni_socket_pair is used to create a socket pair using socketpair()
// on POSIX systems.  (Windows might provide a similar solution, using
// AF_UNIX at some point, in which case the arguments will actually be
// an array of HANDLEs.)  If not supported, this returns NNG_ENOTSUP.
//
// This API can only create a pair of open file descriptors, suitable for use
// with the socket transport, each bound to the other.  The transport must be
// a bidirectional reliable byte stream.  This should be suitable for use
// in APIs to transport file descriptors, or across a fork/exec boundary (so
// that child processes may use these with socket to inherit a socket that is
// connected to the parent.)
extern int nni_socket_pair(int[2]);

//
// File/Store Support
//
// Some transports require a persistent storage for things like
// key material, etc.  Generally, these are all going to be relatively
// small objects (such as certificates), so we only require a synchronous
// implementation from platforms.  This Key-Value API is intended to
// to support using the Key's as filenames, and keys will consist of
// only these characters: [0-9a-zA-Z._-].  The directory used should be
// determined by using an environment variable (NNG_STATE_DIR), or
// using some other application-specific method.
//
// We also support listing keys, for the case where a key must be looked
// up -- for example to get a list of certificates, or some such.
//

// nni_plat_file_put writes the named file, with the provided data,
// and the given size.  If the file already exists it is overwritten.
// The permissions on the file should be limited to read and write
// access by the entity running the application only.
extern int nni_plat_file_put(const char *, const void *, size_t);

// nni_plat_file_get reads the entire named file, allocating storage
// to receive the data and returning the data and the size in the
// reference arguments.  The data pointer should be freed with nni_free
// using the supplied size when no longer needed.
extern int nni_plat_file_get(const char *, void **, size_t *);

// nni_plat_file_delete deletes the named file.  If the name refers to
// a directory, then that will be removed only if empty.
extern int nni_plat_file_delete(const char *);

// nni_plat_file_check checks the file path to determine its type.
// If the path does not exist, then NNG_ENOENT is returned.
enum nni_plat_file_type_val {
	NNI_PLAT_FILE_TYPE_FILE,  // normal file
	NNI_PLAT_FILE_TYPE_DIR,   // normal directory
	NNI_PLAT_FILE_TYPE_OTHER, // something else (pipe, device node, etc.)
};
extern int nni_plat_file_type(const char *, int *);

enum nni_plat_file_walk_result {
	NNI_PLAT_FILE_WALK_CONTINUE,
	NNI_PLAT_FILE_WALK_STOP,        // stop walking (all done)
	NNI_PLAT_FILE_WALK_PRUNE_SIB,   // skip siblings and their children
	NNI_PLAT_FILE_WALK_PRUNE_CHILD, // skip children
};

enum nni_plat_file_walk_flags {
	NNI_PLAT_FILE_WALK_DEPTH_FIRST   = 0, // get children first
	NNI_PLAT_FILE_WALK_BREADTH_FIRST = 1, // get siblings first (later)
	NNI_PLAT_FILE_WALK_SHALLOW = 2, // do not descend into subdirectories
	NNI_PLAT_FILE_WALK_FILES_ONLY = 4, // directory names are not reported
};

// nni_plat_file_walker is called for each pathname found by walking a
// directory tree.  It returns one of the nni_plat_file_walk_result values.
typedef int (*nni_plat_file_walker)(const char *, void *);

// nni_plat_file_walk walks a directory tree, calling the walker function
// with the path name, and the supplied void * argument.
extern int nni_plat_file_walk(const char *, nni_plat_file_walker, void *, int);

typedef struct nni_plat_flock nni_plat_flock;

// nni_plat_file_lock locks the file.  This usually means open it (creating
// if it does not exist) and doing a lock operation.  The nni_plat_flock
// is our handle for the lock, to unlock.  Usually its just a file descriptor,
// and we can unlock by doing close().  Note that this is a "try-lock"
// operation -- if the file is already locked then NNG_EBUSY is returned.
extern int nni_plat_file_lock(const char *path, nni_plat_flock *);

// nni_plat_file_unlock unlocks the previously locked file.
extern void nni_plat_file_unlock(nni_plat_flock *);

// nni_plat_dir_open attempts to "open a directory" for listing.  The
// handle for further operations is returned in the first argument, and
// the directory name is supplied in the second.
extern int nni_plat_dir_open(void **, const char *);

// nni_plat_dir_next gets the next directory entry.  Each call returns
// a new entry (arbitrary order).  When no more entries exist, it returns
// NNG_ENOENT.  The returned name is valid until the next call to this
// function, or until the directory is closed.  Only files are returned,
// subdirectories are not reported.
extern int nni_plat_dir_next(void *, const char **);

// nni_plat_dir_close closes the directory handle, freeing all
// resources associated with it.
extern void nni_plat_dir_close(void *);

// nni_plat_dir_create creates a directory.  Any parent directories must
// already exist.  If the directory already exists, 0 is returned.
extern int nni_plat_dir_create(const char *);

// nni_plat_dir_remove removes a directory, which must already be empty.
// If it does not exist, 0 is returned.
extern int nni_plat_dir_remove(const char *);

// nni_plat_temp_dir returns a temporary/scratch directory for the platform
// The result should be freed with nni_strfree().
extern char *nni_plat_temp_dir(void);

// nni_plat_join_dir joins to path components to make a path name.
// For example. on UNIX systems nni_plat_join_dir("/tmp", "a") returns
// "/tmp/a".  The pathname returned should be freed with nni_strfree().
extern char *nni_plat_join_dir(const char *, const char *);

// nni_plat_file_basename returns the "file" part of the file name.
// The returned pointer will usually reference the end of the supplied
// string, and may not be altered.
extern const char *nni_plat_file_basename(const char *);

//
// Actual platforms we support.  This is included up front so that we can
// get the specific types that are supplied by the platform.
#if defined(NNG_PLATFORM_POSIX)
#include "platform/posix/posix_impl.h"
#elif defined(NNG_PLATFORM_WINDOWS)
#include "platform/windows/win_impl.h"
#else
#error "unknown platform"
#endif

#endif // CORE_PLATFORM_H
