/*
 * This file is open source software, licensed to you under the terms
 * of the Apache License, Version 2.0 (the "License").  See the NOTICE file
 * distributed with this work for additional information regarding copyright
 * ownership.  You may not use this file except in compliance with the License.
 *
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
/*
 * Copyright (C) 2014 Cloudius Systems, Ltd.
 */

#ifdef SEASTAR_MODULE
module;
#include <cstdint>
#include <utility>
#include <cstring>
#include <exception>
#include <system_error>
#include <optional>
#include <memory>
module seastar;
#else
#include <seastar/net/ip.hh>
#include <seastar/net/stack.hh>
#include <seastar/net/inet_address.hh>
#endif

namespace seastar {

using namespace net;

namespace net {
namespace ipv4_udp_impl {

static inline
ipv4_addr
to_ipv4_addr(ipv4_address a, uint16_t port) {
    return {a.ip, port};
}

class native_datagram : public datagram_impl {
private:
    ipv4_addr _src;
    ipv4_addr _dst;
    packet _p;
public:
    native_datagram(ipv4_address src, ipv4_address dst, packet p)
            : _p(std::move(p)) {
        udp_hdr* hdr = _p.get_header<udp_hdr>();
        auto h = ntoh(*hdr);
        _p.trim_front(sizeof(*hdr));
        _src = to_ipv4_addr(src, h.src_port);
        _dst = to_ipv4_addr(dst, h.dst_port);
    }

    virtual socket_address get_src() override {
        return _src;
    };

    virtual socket_address get_dst() override {
        return _dst;
    };

    virtual uint16_t get_dst_port() override {
        return _dst.port;
    }

    virtual packet& get_data() override {
        return _p;
    }
};

class native_channel : public datagram_channel_impl {
private:
    ipv4_udp& _proto;
    ipv4_udp::registration _reg;
    bool _closed;
    lw_shared_ptr<udp_channel_state> _state;

public:
    native_channel(ipv4_udp &proto, ipv4_udp::registration reg, lw_shared_ptr<udp_channel_state> state)
            : _proto(proto)
            , _reg(reg)
            , _closed(false)
            , _state(state)
    {
    }

    ~native_channel()
    {
        if (!_closed)
            close();
    }

    socket_address local_address() const override {
        return socket_address(_proto.inet().host_address(), _reg.port());
    }

    virtual future<datagram> receive() override {
        return _state->_queue.pop_eventually();
    }

    virtual future<> send(const socket_address& dst, const char* msg) override {
        return send(dst, packet::from_static_data(msg, strlen(msg)));
    }

    virtual future<> send(const socket_address& dst, packet p) override {
        auto len = p.len();
        return _state->wait_for_send_buffer(len).then([this, dst, p = std::move(p), len] () mutable {
            p = packet(std::move(p), make_deleter([s = _state, len] { s->complete_send(len); }));
            _proto.send(_reg.port(), dst, std::move(p));
        });
    }

    virtual bool is_closed() const override {
        return _closed;
    }

    virtual void shutdown_input() override {
        _state->_queue.abort(std::make_exception_ptr(std::system_error(EBADF, std::system_category())));
    }

    virtual void shutdown_output() override {
        _state->_queue.abort(std::make_exception_ptr(std::system_error(EPIPE, std::system_category())));
    }

    virtual void close() override {
        _reg.unregister();
        _closed = true;
    }
};

} /* namespace ipv4_udp_impl */

using namespace net::ipv4_udp_impl;

const int ipv4_udp::default_queue_size = 1024;

ipv4_udp::ipv4_udp(ipv4& inet)
    : _inet(inet)
{
    _inet.register_packet_provider([this] {
        std::optional<ipv4_traits::l4packet> l4p;
        if (!_packetq.empty()) {
            l4p = std::move(_packetq.front());
            _packetq.pop_front();
        }
        return l4p;
    });
}

bool ipv4_udp::forward(forward_hash& out_hash_data, packet& p, size_t off)
{
    auto uh = p.get_header<udp_hdr>(off);

    if (uh) {
        out_hash_data.push_back(uh->src_port);
        out_hash_data.push_back(uh->dst_port);
    }
    return true;
}

void ipv4_udp::received(packet p, ipv4_address from, ipv4_address to)
{
    datagram dgram(std::make_unique<native_datagram>(from, to, std::move(p)));

    auto chan_it = _channels.find(dgram.get_dst_port());
    if (chan_it != _channels.end()) {
        auto chan = chan_it->second;
        chan->_queue.push(std::move(dgram));
    }
}

void ipv4_udp::send(uint16_t src_port, ipv4_addr dst, packet &&p)
{
    auto src = _inet.host_address();
    auto hdr = p.prepend_header<udp_hdr>();
    hdr->src_port = src_port;
    hdr->dst_port = dst.port;
    hdr->len = p.len();
    *hdr = hton(*hdr);

    offload_info oi;
    checksummer csum;
    ipv4_traits::udp_pseudo_header_checksum(csum, src, dst, p.len());
    bool needs_frag = ipv4::needs_frag(p, ip_protocol_num::udp, _inet.hw_features());
    if (_inet.hw_features().tx_csum_l4_offload && !needs_frag) {
        hdr->cksum = ~csum.get();
        oi.needs_csum = true;
    } else {
        csum.sum(p);
        hdr->cksum = csum.get();
        oi.needs_csum = false;
    }
    oi.protocol = ip_protocol_num::udp;
    p.set_offload_info(oi);

    // FIXME: future is discarded
    (void)_inet.get_l2_dst_address(dst).then([this, dst, p = std::move(p)] (ethernet_address e_dst) mutable {
        _packetq.emplace_back(ipv4_traits::l4packet{dst, std::move(p), e_dst, ip_protocol_num::udp});
    });
}

uint16_t ipv4_udp::next_port(uint16_t port) {
    return (port == std::numeric_limits<decltype(port)>::max()) ? min_anonymous_port : port + 1;
}

udp_channel
ipv4_udp::make_channel(ipv4_addr addr) {
    if (!is_ip_unspecified(addr)) {
        throw std::runtime_error("Binding to specific IP not supported yet");
    }

    uint16_t bind_port;

    if (!is_port_unspecified(addr)) {
        if (_channels.count(addr.port)) {
            throw std::runtime_error("Address already in use");
        }
        bind_port = addr.port;
    } else {
        auto starting_port = _next_anonymous_port;
        while (_channels.count(_next_anonymous_port)) {
            _next_anonymous_port = next_port(_next_anonymous_port);
            if (starting_port == _next_anonymous_port) {
                throw std::runtime_error("No free port");
            }
        }

        bind_port = _next_anonymous_port;
        _next_anonymous_port = next_port(_next_anonymous_port);
    }

    auto chan_state = make_lw_shared<udp_channel_state>(_queue_size);
    _channels[bind_port] = chan_state;
    return udp_channel(std::make_unique<native_channel>(*this, registration(*this, bind_port), chan_state));
}

} /* namespace net */

}

