#pragma once
#include "util.h"
#include "Stream.h"
#include "AddrInfo.h"
#include "Resolver.h"
#include "backend/TcpImpl.h"
#include <iosfwd>

namespace panda { namespace unievent {

struct ITcpListener     : IStreamListener     {};
struct ITcpSelfListener : IStreamSelfListener {};

struct Tcp : virtual Stream, AllocatedObject<Tcp> {
    using TcpImpl = backend::TcpImpl;
    using Flags   = TcpImpl::Flags;

    static const HandleType TYPE;
    static const AddrInfoHints defhints;

    Tcp (const LoopSP& loop = Loop::default_loop(), int domain = AF_UNSPEC);

    ~Tcp () { panda_log_dtor(); }

    const HandleType& type () const override;

    virtual excepted<void, ErrorCode> open (sock_t socket, Ownership = Ownership::TRANSFER);
    virtual excepted<void, ErrorCode> bind (const net::SockAddr&, unsigned flags = 0);
    virtual excepted<void, ErrorCode> bind (string_view host, uint16_t port, const AddrInfoHints& hints = defhints, unsigned flags = 0);

    TcpConnectRequestSP connect ();
    TcpConnectRequestSP connect (const net::SockAddr& sa, uint64_t timeout = 0);
    TcpConnectRequestSP connect (const string& host, uint16_t port, uint64_t timeout = 0, const AddrInfoHints& hints = defhints);

    virtual void connect (const TcpConnectRequestSP&);

    excepted<net::SockAddr, ErrorCode> sockaddr () const override;
    excepted<net::SockAddr, ErrorCode> peeraddr () const override;

    excepted<void, ErrorCode> set_nodelay              (bool enable)                     { return make_excepted(impl()->set_nodelay(enable)); }
    excepted<void, ErrorCode> set_keepalive            (bool enable, unsigned int delay) { return make_excepted(impl()->set_keepalive(enable, delay)); }
    excepted<void, ErrorCode> set_simultaneous_accepts (bool enable)                     { return make_excepted(impl()->set_simultaneous_accepts(enable)); }

    excepted<sock_t, ErrorCode> socket () const;

    void setsockopt (int level, int optname, const void* optval, int optlen) { unievent::setsockopt(socket().value(), level, optname, optval, optlen); }

    static excepted<std::pair<TcpSP, TcpSP>, ErrorCode>
    pair (const LoopSP& = Loop::default_loop(), int type = SOCK_STREAM, int proto = PF_UNSPEC);

    static excepted<std::pair<TcpSP, TcpSP>, ErrorCode>
    pair (const TcpSP&, const TcpSP&, int type = SOCK_STREAM, int proto = PF_UNSPEC);

protected:
    StreamSP create_connection () override;

private:
    friend TcpConnectRequest;

    int domain;

    backend::TcpImpl* impl () const { return static_cast<backend::TcpImpl*>(BackendHandle::impl()); }

    HandleImpl* new_impl () override;
};

struct TcpConnectRequest : ConnectRequest, AllocatedObject<TcpConnectRequest> {
    net::SockAddr addr;
    string        host;
    uint16_t      port;
    bool          cached;
    AddrInfoHints hints;

    TcpConnectRequest (Tcp* h = nullptr) : port(), cached(true), handle(h) {}

    TcpConnectRequestSP to (const string& host, uint16_t port, const AddrInfoHints& hints = Tcp::defhints) {
        this->host  = host;
        this->port  = port;
        this->hints = hints;
        return this;
    }

    TcpConnectRequestSP to         (const net::SockAddr& val)      { addr = val; return this; }
    TcpConnectRequestSP timeout    (uint64_t val)                  { this->ConnectRequest::timeout = val; return this; }
    TcpConnectRequestSP on_connect (const Stream::connect_fn& val) { event.add(val); return this; }
    TcpConnectRequestSP use_cache  (bool val)                      { cached = val; return this; }
    TcpConnectRequestSP set_hints  (const AddrInfoHints& val)      { hints = val; return this; }
    TcpConnectRequestSP run        ()                              { handle->connect(this); return this; }

private:
    friend Tcp; friend StreamFilter;
    Tcp*                handle;
    Resolver::RequestSP resolve_request;

    void set (Tcp* h) {
        handle = h;
        ConnectRequest::set(h);
    }

    void exec             () override;
    void finalize_connect ();
    void handle_event     (const ErrorCode&) override;
};

inline TcpConnectRequestSP Tcp::connect () {
    return new TcpConnectRequest(this);
}

inline TcpConnectRequestSP Tcp::connect (const net::SockAddr& addr, uint64_t timeout) {
    return connect()->to(addr)->timeout(timeout)->run();
}

inline TcpConnectRequestSP Tcp::connect (const string& host, uint16_t port, uint64_t timeout, const AddrInfoHints& hints) {
    return connect()->to(host, port, hints)->timeout(timeout)->run();
}

std::ostream& operator<< (std::ostream&, const Tcp&);
std::ostream& operator<< (std::ostream&, const TcpConnectRequest&);

}}