//    OpenVPN -- An application to securely tunnel IP networks
//               over a single port, with support for SSL/TLS-based
//               session authentication and key exchange,
//               packet encryption, packet authentication, and
//               packet compression.
//
//    Copyright (C) 2012- OpenVPN Inc.
//
//    SPDX-License-Identifier: MPL-2.0 OR AGPL-3.0-only WITH openvpn3-openssl-exception
//

#ifndef OPENVPN_ADDR_ADDRPAIR_H
#define OPENVPN_ADDR_ADDRPAIR_H

#include <sstream>

#include <openvpn/common/exception.hpp>
#include <openvpn/common/number.hpp>
#include <openvpn/common/split.hpp>
#include <openvpn/addr/ip.hpp>

namespace openvpn::IP {

// AddrMaskPair is basically an object that combines an IP address (v4 or v6)
// with a netmask or prefix length.
struct AddrMaskPair
{
  public:
    OPENVPN_EXCEPTION(addr_pair_mask_parse_error);

    class StringPair
    {
      public:
        OPENVPN_SIMPLE_EXCEPTION(addr_pair_string_error);

        StringPair()
            : size_(0)
        {
        }

        explicit StringPair(const std::string &s1)
            : size_(1)
        {
            data[0] = s1;
        }

        explicit StringPair(const std::string &s1, const std::string &s2)
            : size_(2)
        {
            data[0] = s1;
            data[1] = s2;
        }

        void push_back(const std::string &s)
        {
            if (size_ < 2)
                data[size_++] = s;
            else
                throw addr_pair_string_error();
        }

        const std::string &operator[](const size_t i) const
        {
            if (i >= 2)
                throw addr_pair_string_error();
            return data[i];
        }

        std::string &operator[](const size_t i)
        {
            if (i >= 2)
                throw addr_pair_string_error();
            return data[i];
        }

        size_t size() const
        {
            return size_;
        }

        std::string render() const
        {
            switch (size_)
            {
            case 1:
                return data[0];
            case 2:
                return data[0] + "/" + data[1];
            default:
                return "";
            }
        }

      private:
        std::string data[2];
        unsigned int size_;
    };

    static AddrMaskPair from_string(const std::string &s1, const std::string &s2, const char *title = nullptr)
    {
        try
        {
            if (s2.empty())
            {
                const StringPair pair = Split::by_char<StringPair, NullLex, Split::NullLimit>(s1, '/');
                return from_string_impl(pair, title);
            }
            else
            {
                const StringPair pair(s1, s2);
                return from_string_impl(pair, title);
            }
        }
        catch (const std::exception &e)
        {
            const StringPair pair(s1, s2);
            error(e, pair.render(), title);
        }
        return AddrMaskPair(); // NOTREACHED
    }

    static AddrMaskPair from_string(const std::string &s, const char *title = nullptr)
    {
        try
        {
            const StringPair pair = Split::by_char<StringPair, NullLex, Split::NullLimit>(s, '/');
            return from_string_impl(pair, title);
        }
        catch (const std::exception &e)
        {
            error(e, s, title);
        }
        return AddrMaskPair(); // NOTREACHED
    }

    static AddrMaskPair from_string(const StringPair &pair, const char *title = nullptr)
    {
        try
        {
            return from_string_impl(pair, title);
        }
        catch (const std::exception &e)
        {
            error(e, pair.render(), title);
        }
        return AddrMaskPair(); // NOTREACHED
    }

    std::string to_string(const bool netmask_form = false) const
    {
        std::ostringstream os;
        if (netmask_form)
            os << addr.to_string() << '/' << netmask.to_string();
        else
            os << addr.to_string() << '/' << netmask.prefix_len();
        return os.str();
    }

    bool is_canonical() const
    {
        return (addr & netmask) == addr;
    }

    Addr::Version version() const
    {
        const Addr::Version v1 = addr.version();
        const Addr::Version v2 = netmask.version();
        if (v1 == v2)
            return v1;
        else
            return Addr::UNSPEC;
    }

    Addr addr;
    Addr netmask;

  private:
    static void error(const std::exception &e, const std::string &s, const char *title)
    {
        if (!title)
            title = "";
        OPENVPN_THROW(addr_pair_mask_parse_error, "AddrMaskPair parse error '" << title << "': " << s << " : " << e.what());
    }

    static AddrMaskPair from_string_impl(const StringPair &pair, const char *title = nullptr)
    {
        AddrMaskPair ret;
        if (pair.size() == 1 || pair.size() == 2)
        {
            ret.addr = Addr::from_string(pair[0], title);
            if (pair.size() == 2 && !pair[1].empty())
            {
                if (is_number(pair[1].c_str()))
                    ret.netmask = Addr::netmask_from_prefix_len(ret.addr.version(),
                                                                parse_number_throw<unsigned int>(pair[1], "prefix length"));
                else
                    ret.netmask = Addr::from_string(pair[1]);
                ret.netmask.prefix_len(); // verify that netmask is ok
            }
            else
                ret.netmask = Addr::from_zero_complement(ret.addr.version());
            ret.addr.verify_version_consistency(ret.netmask);
        }
        else
            throw addr_pair_mask_parse_error("only one or two address terms allowed");
        return ret;
    }
};
OPENVPN_OSTREAM(AddrMaskPair, to_string)
} // namespace openvpn::IP

#endif
