package ru.yandex.wmtools.common.data.info;

import ru.yandex.wmtools.common.error.UserException;
import ru.yandex.wmtools.common.error.UserProblem;

import java.math.BigInteger;

/**
 * @author avhaliullin
 */
public class IPv4Info extends IPInfo {
    //package-private:
    static final int FULL_MASK_LENGTH = IPvEnum.IPv4.getMaxMaskLength();
    static final long MAX_IP_ADDRESS = 256l * 256 * 256 * 256 - 1;

    private static boolean USE_NULL_FOR_HIGHEST_64 = false;

    private final long address;

    private String addrStringCache = null;

    public IPv4Info(long longForm, int maskLength) throws UserException {
        super(maskLength);
        this.address = longForm;
        checkAddress();
    }

    public IPv4Info(String stringForm, Integer maskLength) throws UserException {
        super(maskLength == null ? FULL_MASK_LENGTH : maskLength);
        try {
            address = (getLong(stringForm) >> (FULL_MASK_LENGTH - this.maskLength)) << (FULL_MASK_LENGTH - this.maskLength);
            checkAddress();
        } catch (NumberFormatException e) {
            throw new UserException(UserProblem.ILLEGAL_PARAM_VALUE, "Wrong ip address or ip subnet: " + stringForm, e);
        }
    }

    private void checkAddress() throws UserException {
        if (address > MAX_IP_ADDRESS) {
            throw new UserException(UserProblem.ILLEGAL_PARAM_VALUE, "IPv4 address has more than 32 bit length");
        }
    }

    private long getLong(String stringForm) throws UserException, NumberFormatException {
        String[] parts = stringForm.split("\\.");
        if (parts.length != 4) {
            throw new UserException(UserProblem.ILLEGAL_PARAM_VALUE, "Wrong ip address or ip subnet: " + stringForm);
        }
        long addressAcc = 0;
        for (String part : parts) {
            addressAcc *= 256;
            int partValue = Integer.parseInt(part);
            if (partValue > 255) {
                throw new UserException(UserProblem.ILLEGAL_PARAM_VALUE, "Wrong ip address or ip subnet: " + stringForm);
            }
            addressAcc += partValue;
        }
        return addressAcc;
    }

    @Override
    protected void checkMaskLength() throws UserException {
        if (maskLength > FULL_MASK_LENGTH) {
            throw new UserException(UserProblem.ILLEGAL_PARAM_VALUE, "Mask length is more than 32: " + maskLength);
        }
    }

    @Override
    public boolean isSingleAddress() {
        return maskLength == FULL_MASK_LENGTH;
    }

    @Override
    public boolean isAllInclusive() {
        return maskLength == 0;
    }

    @Override
    public IPvEnum getProtocolVersion() {
        return IPvEnum.IPv4;
    }

    @Override
    public String getAddressAsString(boolean withDimension) {
        if (addrStringCache != null) {
            return addrStringCache;
        }
        StringBuilder res = new StringBuilder();
        res
                .append(address / (256 * 256 * 256))
                .append(".")
                .append((address / (256 * 256)) % 256)
                .append(".")
                .append((address / 256) % 256)
                .append(".")
                .append(address % 256);
        if (withDimension && !isSingleAddress()) {
            res.append("/")
                    .append(maskLength);
        }
        addrStringCache = res.toString();
        return addrStringCache;
    }

    @Override
    public boolean matches(IPInfo ip) {
        if (!(ip instanceof IPv4Info)) {
            return ip instanceof IPv6Info && matches(((IPv6Info) ip).castToIPv4());
        }
        IPv4Info info = (IPv4Info) ip;
        return info.getMaskLength() >= maskLength && info.address >> (FULL_MASK_LENGTH - maskLength) == address >> (FULL_MASK_LENGTH - maskLength);
    }

    @Override
    public BigInteger getLower64() {
        return BigInteger.valueOf(address);
    }

    @Override
    public BigInteger getHigher64() {
        return USE_NULL_FOR_HIGHEST_64 ? null : IPv6Info.RESERVED_64_BITS;
    }

    @Override
    public boolean equals(Object o) {
        if (o == null) {
            return false;
        }
        if (!(o instanceof IPv4Info)) {
            return false;
        }
        IPv4Info info = (IPv4Info) o;
        return info.address == address && info.maskLength == maskLength;
    }

    public IPv6Info castToIPv6() {
        try {
            return new IPv6Info(IPv6Info.IPV4_BASE.or(BigInteger.valueOf(address)), maskLength + IPv6Info.FULL_MASK_LENGTH - FULL_MASK_LENGTH);
        } catch (UserException e) {
            throw new AssertionError("Unbelievable - failed to cast IPv4 to IPv6 " + getAddressAsString(true));
        }
    }

    public static void compatibilityMode() {
        USE_NULL_FOR_HIGHEST_64 = true;
    }
}
