#pragma once
#include <panda/string.h>
#include <panda/string_view.h>
#include <utility>
#ifdef _WIN32
    #include <winsock2.h>
    #include <Ws2tcpip.h>
#else
    #include <netinet/in.h>
    #include <sys/socket.h>
    #include <sys/un.h>
#endif

namespace panda { namespace net {

using sa_family_t = decltype(std::declval<sockaddr>().sa_family);

struct SockAddr {
    struct Inet4;
    struct Inet6;

    static const size_t IP4_MAX_ADDRSTRLEN = 16;
    static const size_t IP6_MAX_ADDRSTRLEN = 46;
    static const size_t BASE_LEN = offsetof(sockaddr, sa_data);

    SockAddr () { sa.sa_family = AF_UNSPEC; }

    SockAddr (const sockaddr* sa, size_t length);
    SockAddr (const sockaddr_in*  sa) : sa4(*sa) {}
    SockAddr (const sockaddr_in6* sa) : sa6(*sa) {}

    SockAddr (const SockAddr& oth) { operator=(oth); }

    SockAddr& operator= (const SockAddr& oth);

    sa_family_t family () const { return sa.sa_family; }

    bool is_inet4 () const { return family() == AF_INET; }
    bool is_inet6 () const { return family() == AF_INET6; }

    const Inet4& as_inet4 () const { return *((const Inet4*)this); }
    const Inet6& as_inet6 () const { return *((const Inet6*)this); }
    Inet4&       as_inet4 ()       { return *((Inet4*)this); }
    Inet6&       as_inet6 ()       { return *((Inet6*)this); }

    const sockaddr* get () const { return &sa; }
    sockaddr*       get ()       { return &sa; }

    bool operator== (const SockAddr& oth) const;
    bool operator!= (const SockAddr& oth) const { return !operator==(oth); }

    explicit
    operator bool () const { return sa.sa_family != AF_UNSPEC; }

    string   ip     () const;
    uint16_t port   () const;
    size_t   length () const;

    template <typename Function>
    void assign_foreign (Function&& fn) {
        size_t length = sizeof(SockAddr); // max size
        bool success = fn(&sa, &length);
        if (success) {
            // length is the actual size
            validate(&sa, length);
            #ifndef _WIN32
            if (sa.sa_family == AF_UNIX) fix_unix_path(length);
            #endif
        }
    }

  #ifndef _WIN32
    static const size_t PATH_OFFSET = offsetof(sockaddr_un, sun_path);
    struct Unix;

    SockAddr (const sockaddr_un* sa, size_t length) : SockAddr((const sockaddr*)sa, length) {}

    bool is_unix () const { return family() == AF_UNIX; }

    Unix& as_unix () const { return *((Unix*)this); }
  #endif

protected:
    union {
        sockaddr     sa;
        sockaddr_in  sa4;
        sockaddr_in6 sa6;
      #ifndef _WIN32
        sockaddr_un  sau;
      #endif
    };

private:
    void validate (const sockaddr*, size_t len);

  #ifndef _WIN32
    void fix_unix_path (size_t length) noexcept;
  #endif
};

std::ostream& operator<< (std::ostream&, const SockAddr&);

struct SockAddr::Inet4 : SockAddr {
    static const in_addr addr_any;
    static const in_addr addr_loopback;
    static const in_addr addr_broadcast;
    static const in_addr addr_none;
    static const Inet4   sockaddr_any;
    static const Inet4   sockaddr_loopback;

    Inet4 (const sockaddr_in* sa) : SockAddr(sa)        {}
    Inet4 (const Inet4& oth)      : SockAddr(oth.get()) {}

    Inet4 (const string_view& ip, uint16_t port);
    Inet4 (const in_addr& addr, uint16_t port);

    const in_addr& addr () const { return sa4.sin_addr; }
    in_addr&       addr ()       { return sa4.sin_addr; }

    const sockaddr_in* get () const { return &sa4; }
    sockaddr_in*       get ()       { return &sa4; }

    uint16_t port () const { return ntohs(sa4.sin_port); }
    string   ip   () const;
};

struct SockAddr::Inet6 : SockAddr {
    static const in6_addr addr_any;
    static const in6_addr addr_loopback;
    static const Inet6    sockaddr_any;
    static const Inet6    sockaddr_loopback;

    Inet6 (const sockaddr_in6* sa) : SockAddr(sa)        {}
    Inet6 (const Inet6& oth)       : SockAddr(oth.get()) {}

    Inet6 (const string_view& ip, uint16_t port, uint32_t scope_id = 0, uint32_t flowinfo = 0);
    Inet6 (const in6_addr& addr, uint16_t port, uint32_t scope_id = 0, uint32_t flowinfo = 0);

    const in6_addr& addr () const { return sa6.sin6_addr; }
    in6_addr&       addr ()       { return sa6.sin6_addr; }

    const sockaddr_in6* get () const { return &sa6; }
    sockaddr_in6*       get ()       { return &sa6; }

    string   ip       () const;
    uint16_t port     () const { return ntohs(sa6.sin6_port); }
    uint32_t scope_id () const { return sa6.sin6_scope_id; }
    uint32_t flowinfo () const { return ntohl(sa6.sin6_flowinfo); }
};

#ifndef _WIN32

struct SockAddr::Unix : SockAddr {
    Unix (const sockaddr_un* sa, size_t length) : SockAddr(sa, length) {}
    Unix (const Unix& oth)                      : SockAddr(oth) {}

    Unix (const string_view& path);

    string_view path () const { return (char*)sau.sun_path; }

    const sockaddr_un* get () const { return &sau; }
    sockaddr_un*       get ()       { return &sau; }
};

#endif

}}