/*
 * Copyright 2012 The Netty Project
 *
 * The Netty Project licenses this file to you under the Apache License,
 * version 2.0 (the "License"); 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.
 */
package org.jboss.netty.handler.ipfilter;

import org.jboss.netty.logging.InternalLogger;
import org.jboss.netty.logging.InternalLoggerFactory;

import java.math.BigInteger;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.UnknownHostException;

/**
 */
public class CIDR6 extends CIDR {

    private static final InternalLogger logger = InternalLoggerFactory.getInstance(CIDR6.class);

    /** The big integer for the base address */
    private BigInteger addressBigInt;

    /** The big integer for the end address */
    private final BigInteger addressEndBigInt;

    protected CIDR6(Inet6Address newaddress, int newmask) {
        cidrMask = newmask;
        addressBigInt = ipv6AddressToBigInteger(newaddress);
        BigInteger mask = ipv6CidrMaskToMask(newmask);
        try {
            addressBigInt = addressBigInt.and(mask);
            baseAddress = bigIntToIPv6Address(addressBigInt);
        } catch (UnknownHostException e) {
            // this should never happen.
        }
        addressEndBigInt = addressBigInt.add(ipv6CidrMaskToBaseAddress(cidrMask)).subtract(BigInteger.ONE);
    }

    @Override
    public InetAddress getEndAddress() {
        try {
            return bigIntToIPv6Address(addressEndBigInt);
        } catch (UnknownHostException e) {
            if (logger.isErrorEnabled()) {
                logger.error("invalid ip address calculated as an end address");
            }
            return null;
        }
    }

    public int compareTo(CIDR arg) {
        if (arg instanceof CIDR4) {
            BigInteger net = ipv6AddressToBigInteger(arg.baseAddress);
            int res = net.compareTo(addressBigInt);
            if (res == 0) {
                if (arg.cidrMask == cidrMask) {
                    return 0;
                }
                if (arg.cidrMask < cidrMask) {
                    return -1;
                }
                return 1;
            }
            return res;
        }
        CIDR6 o = (CIDR6) arg;
        if (o.addressBigInt.equals(addressBigInt) && o.cidrMask == cidrMask) {
            return 0;
        }
        int res = o.addressBigInt.compareTo(addressBigInt);
        if (res == 0) {
            if (o.cidrMask < cidrMask) {
                // greater Mask means less IpAddresses so -1
                return -1;
            }
            return 1;
        }
        return res;
    }

    @Override
    public boolean contains(InetAddress inetAddress) {
        if (inetAddress == null) {
            throw new NullPointerException("inetAddress");
        }

        if (cidrMask == 0) {
            return true;
        }

        BigInteger search = ipv6AddressToBigInteger(inetAddress);
        return search.compareTo(addressBigInt) >= 0 && search.compareTo(addressEndBigInt) <= 0;
    }

    /**
     * Given an IPv6 baseAddress length, return the block length.  I.e., a
     * baseAddress length of 96 will return 2**32.
     */
    private static BigInteger ipv6CidrMaskToBaseAddress(int cidrMask) {
        return BigInteger.ONE.shiftLeft(128 - cidrMask);
    }

    private static BigInteger ipv6CidrMaskToMask(int cidrMask) {
        return BigInteger.ONE.shiftLeft(128 - cidrMask).subtract(BigInteger.ONE).not();
    }

    /**
     * Given an IPv6 address, convert it into a BigInteger.
     *
     * @return the integer representation of the InetAddress
     * @throws IllegalArgumentException if the address is not an IPv6
     *                                  address.
     */
    private static BigInteger ipv6AddressToBigInteger(InetAddress addr) {
        byte[] ipv6;
        if (addr instanceof Inet4Address) {
            ipv6 = getIpV6FromIpV4((Inet4Address) addr);
        } else {
            ipv6 = addr.getAddress();
        }
        if (ipv6[0] == -1) {
            return new BigInteger(1, ipv6);
        }
        return new BigInteger(ipv6);
    }

    /**
     * Convert a big integer into an IPv6 address.
     *
     * @return the inetAddress from the integer
     * @throws UnknownHostException if the big integer is too large,
     *                              and thus an invalid IPv6 address.
     */
    private static InetAddress bigIntToIPv6Address(BigInteger addr) throws UnknownHostException {
        byte[] a = new byte[16];
        byte[] b = addr.toByteArray();
        if (b.length > 16 && !(b.length == 17 && b[0] == 0)) {
            throw new UnknownHostException("invalid IPv6 address (too big)");
        }
        if (b.length == 16) {
            return InetAddress.getByAddress(b);
        }
        // handle the case where the IPv6 address starts with "FF".
        if (b.length == 17) {
            System.arraycopy(b, 1, a, 0, 16);
        } else {
            // copy the address into a 16 byte array, zero-filled.
            int p = 16 - b.length;
            System.arraycopy(b, 0, a, p, b.length);
        }
        return InetAddress.getByAddress(a);
    }
}
