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 abstract class IPInfo implements Comparable<IPInfo> {
    protected final int maskLength;

    public static IPInfo createFromString(String address, Integer maskLength) throws UserException {
        if (address.contains(":")) {
            IPv6Info res = new IPv6Info(address, maskLength);
            if (res.isCorrectIPv4()) {
                return res.castToIPv4();
            } else {
                return res;
            }
        } else {
            return new IPv4Info(address, maskLength);
        }
    }

    public static IPInfo createFromString(String address) throws UserException {
        Integer mask = null;
        if (address.contains("/")) {
            try {
                String[] s = address.split("/");
                mask = Integer.parseInt(s[1]);
                address = s[0];
            } catch (NumberFormatException e) {
                throw new UserException(UserProblem.ILLEGAL_PARAM_VALUE, "Bad ip subnet " + address);
            }
        }
        return createFromString(address, mask);
    }

    public static IPInfo createFromBigInt(BigInteger higher64, BigInteger lower64, int maskLength) throws UserException {
        if (higher64 == null || IPv6Info.RESERVED_64_BITS.equals(higher64)) {
            return new IPv4Info(lower64.longValue(), maskLength);
        } else {
            return new IPv6Info(higher64, lower64, maskLength);
        }
    }

    protected IPInfo(int maskLength) throws UserException {
        this.maskLength = maskLength;
        checkMaskLength();
    }

    public int getMaskLength() {
        return maskLength;
    }

    public String getAddressAsString() {
        return getAddressAsString(true);
    }

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

    public abstract boolean isSingleAddress();

    public abstract IPvEnum getProtocolVersion();

    public abstract String getAddressAsString(boolean withDimension);


    public boolean matches(String ip) throws UserException {
        return matches(createFromString(ip));
    }

    public int hashCode() {
        return getLower64().hashCode() +
                (getHigher64() == null ? 0 : getHigher64().hashCode()) +
                Integer.MAX_VALUE / (maskLength + 1);
    }

    @Override
    public int compareTo(IPInfo ipInfo) {
        int tmpResult = getProtocolVersion().version - ipInfo.getProtocolVersion().version;
        if (tmpResult != 0) {
            return tmpResult;
        }
        tmpResult = getAddress().compareTo(ipInfo.getAddress());
        if (tmpResult != 0) {
            return tmpResult;
        }
        return maskLength - ipInfo.getMaskLength();
    }


    public String toString() {
        return getClass().getSimpleName() + "(" + getAddressAsString() + ")";
    }

    public abstract boolean matches(IPInfo ip);

    public abstract BigInteger getLower64();

    public abstract BigInteger getHigher64();

    public BigInteger getAddress() {
        if (getHigher64() == null) {
            return getLower64();
        } else {
            return getHigher64().shiftLeft(64).add(getLower64());
        }
    }

    protected abstract void checkMaskLength() throws UserException;

    public enum IPvEnum {
        IPv4(32, 4),
        IPv6(128, 6);
        private final int maxMaskLength;
        private final int version;

        IPvEnum(int maxMaskLength, int version) {
            this.maxMaskLength = maxMaskLength;
            this.version = version;
        }

        public int getMaxMaskLength() {
            return maxMaskLength;
        }

        public int getVersion() {
            return version;
        }
    }
}
