package ru.kirillius.pf.sdn.api.Networking;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;
import lombok.Getter;
import ru.kirillius.pf.sdn.Util.IPv4Util;

import java.net.Inet4Address;
import java.util.Objects;
import java.util.regex.Pattern;

/**
 * Immutable representation of an IPv4 subnet serialized as a CIDR string.
 */
public class IPv4Subnet {

    @Getter
    private final long longAddress;
    @Getter
    private final int prefixLength;

    /**
     * Compares two subnets for equality based on address and prefix length.
     */
    @Override
    public boolean equals(Object o) {
        if (o == null || getClass() != o.getClass()) return false;
        IPv4Subnet that = (IPv4Subnet) o;
        return longAddress == that.longAddress && prefixLength == that.prefixLength;
    }

    /**
     * Computes a hash code using address and prefix length.
     */
    @Override
    public int hashCode() {
        return Objects.hash(longAddress, prefixLength);
    }

    /**
     * Parses a subnet from textual CIDR notation.
     */
    @JsonCreator
    public IPv4Subnet(String subnet) {
        var split = subnet.split(Pattern.quote("/"));
        if (split.length != 2) {
            throw new IllegalArgumentException("Invalid subnet: " + subnet);
        }
        var prefix = Integer.parseInt(split[1]);
        IPv4Util.validatePrefix(prefix);

        longAddress = IPv4Util.ipAddressToLong(split[0]);
        prefixLength = prefix;
    }

    public IPv4Subnet(Inet4Address address, int prefixLength) {
        IPv4Util.validatePrefix(prefixLength);
        longAddress = IPv4Util.ipAddressToLong(address.getHostAddress());
        this.prefixLength = prefixLength;
    }

    /**
     * Creates a subnet from a numeric address and prefix length.
     */
    public IPv4Subnet(long longAddress, int prefixLength) {
        this.longAddress = longAddress;
        this.prefixLength = prefixLength;
    }

    /**
     * Creates a subnet from a dotted address string and prefix length.
     */
    public IPv4Subnet(String address, int prefixLength) {
        IPv4Util.validatePrefix(prefixLength);

        this.longAddress = IPv4Util.ipAddressToLong(address);
        this.prefixLength = prefixLength;
    }

    /**
     * Calculates the number of addresses within the subnet.
     */
    public long count() {
        return IPv4Util.calculateCountForPrefixLength(prefixLength);
    }

    /**
     * Returns the dotted-decimal representation of the subnet's network address.
     */
    public String getAddress() {
        return IPv4Util.longToIpAddress(longAddress);
    }

    /**
     * Formats the subnet using CIDR notation.
     */
    @JsonValue
    @Override
    public String toString() {
        return getAddress() + '/' + prefixLength;
    }

    /**
     * Determines whether this subnet overlaps with another.
     */
    public boolean overlaps(IPv4Subnet subnet) {
        var minPrefixLength = Math.min(prefixLength, subnet.prefixLength);
        var commonMask = IPv4Util.calculateMask(minPrefixLength);
        if (minPrefixLength != prefixLength) {
            return false; //can't overlap larger prefix
        }

        return (longAddress & commonMask) == (subnet.longAddress & commonMask);
    }

    public boolean contains(String address) {
        var longAddress = IPv4Util.ipAddressToLong(address);
        var commonMask = IPv4Util.calculateMask(prefixLength);
        return (this.longAddress & commonMask) == (longAddress & commonMask);
    }

}
