/* Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF 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.apache.harmony.luni.util;

import java.util.ArrayList;
import java.util.StringTokenizer;

/**
 * Utility functions for IPV6 operations.
 */
public class Inet6Util {

    /**
     * Creates an byte[] based on an ipAddressString. No error handling is
     * performed here.
     */
    public static byte[] createByteArrayFromIPAddressString(
            String ipAddressString) {

        if (isValidIPV4Address(ipAddressString)) {
            StringTokenizer tokenizer = new StringTokenizer(ipAddressString, ".");
            String token = "";
            int tempInt = 0;
            byte[] byteAddress = new byte[4];
            for (int i = 0; i < 4; i++) {
                token = tokenizer.nextToken();
                tempInt = Integer.parseInt(token);
                byteAddress[i] = (byte) tempInt;
            }

            return byteAddress;
        }
        
        if (ipAddressString.charAt(0) == '[') {
            ipAddressString = ipAddressString.substring(1, ipAddressString.length() - 1);
        }

        StringTokenizer tokenizer = new StringTokenizer(ipAddressString, ":.", true);
        ArrayList<String> hexStrings = new ArrayList<String>();
        ArrayList<String> decStrings = new ArrayList<String>();
        String token = "";
        String prevToken = "";
        // If a double colon exists, we need to insert 0s.
        int doubleColonIndex = -1;

        /*
         * Go through the tokens, including the separators ':' and '.' When we
         * hit a : or . the previous token will be added to either the hex list
         * or decimal list. In the case where we hit a :: we will save the index
         * of the hexStrings so we can add zeros in to fill out the string
         */
        while (tokenizer.hasMoreTokens()) {
            prevToken = token;
            token = tokenizer.nextToken();

            if (token.equals(":")) {
                if (prevToken.equals(":")) {
                    doubleColonIndex = hexStrings.size();
                } else if (!prevToken.equals("")) {
                    hexStrings.add(prevToken);
                }
            } else if (token.equals(".")) {
                decStrings.add(prevToken);
            }
        }

        if (prevToken.equals(":")) {
            if (token.equals(":")) {
                doubleColonIndex = hexStrings.size();
            } else {
                hexStrings.add(token);
            }
        } else if (prevToken.equals(".")) {
            decStrings.add(token);
        }

        // figure out how many hexStrings we should have
        // also check if it is a IPv4 address
        int hexStringsLength = 8;

        // If we have an IPv4 address tagged on at the end, subtract
        // 4 bytes, or 2 hex words from the total
        if (decStrings.size() > 0) {
            hexStringsLength -= 2;
        }

        // if we hit a double Colon add the appropriate hex strings
        if (doubleColonIndex != -1) {
            int numberToInsert = hexStringsLength - hexStrings.size();
            for (int i = 0; i < numberToInsert; i++) {
                hexStrings.add(doubleColonIndex, "0");
            }
        }

        byte ipByteArray[] = new byte[16];

        // Finally convert these strings to bytes...
        for (int i = 0; i < hexStrings.size(); i++) {
            convertToBytes(hexStrings.get(i), ipByteArray, i * 2);
        }

        // Now if there are any decimal values, we know where they go...
        for (int i = 0; i < decStrings.size(); i++) {
            ipByteArray[i + 12] = (byte) (Integer.parseInt(decStrings.get(i)) & 255);
        }

        // now check to see if this guy is actually and IPv4 address
        // an ipV4 address is ::FFFF:d.d.d.d
        boolean ipV4 = true;
        for (int i = 0; i < 10; i++) {
            if (ipByteArray[i] != 0) {
                ipV4 = false;
                break;
            }
        }

        if (ipByteArray[10] != -1 || ipByteArray[11] != -1) {
            ipV4 = false;
        }

        if (ipV4) {
            byte ipv4ByteArray[] = new byte[4];
            for (int i = 0; i < 4; i++) {
                ipv4ByteArray[i] = ipByteArray[i + 12];
            }
            return ipv4ByteArray;
        }
        
        return ipByteArray;

    }

    // BEGIN android-changed
    static char[] hexCharacters = {'0', '1', '2', '3', '4', '5', '6', '7', '8',
            '9', 'a', 'b', 'c', 'd', 'e', 'f'};
    // END android-changed

    public static String createIPAddrStringFromByteArray(byte ipByteArray[]) {
        if (ipByteArray.length == 4) {
            return addressToString(bytesToInt(ipByteArray, 0));
        }

        if (ipByteArray.length == 16) {
            if (isIPv4MappedAddress(ipByteArray)) {
                byte ipv4ByteArray[] = new byte[4];
                for (int i = 0; i < 4; i++) {
                    ipv4ByteArray[i] = ipByteArray[i + 12];
                }
                return addressToString(bytesToInt(ipv4ByteArray, 0));
            }
            StringBuilder buffer = new StringBuilder();
            // BEGIN android-changed
            for (int i = 0; i < 8; i++) { // ipByteArray.length / 2

                int num = (ipByteArray[2 * i] & 0xff) << 8;
                num ^= ipByteArray[2 * i + 1] & 0xff;

                // count the digits to display without leading 0
                int count = 1, j = num;
                while ((j >>>= 4) != 0) {
                    count++;
                }

                char[] buf = new char[count];
                do {
                    int t = num & 0x0f;
                    buf[--count] = hexCharacters[t];
                    num >>>= 4;
                } while (count > 0);

                buffer.append(buf);
                if ((i + 1) < 8) { // ipByteArray.length / 2
                    buffer.append(":");
                }
            }
            // END android-changed
            return buffer.toString();
        }
        return null;
    }

    /** Converts a 4 character hex word into a 2 byte word equivalent */
    public static void convertToBytes(String hexWord, byte ipByteArray[],
            int byteIndex) {

        int hexWordLength = hexWord.length();
        int hexWordIndex = 0;
        ipByteArray[byteIndex] = 0;
        ipByteArray[byteIndex + 1] = 0;
        int charValue;

        // high order 4 bits of first byte
        if (hexWordLength > 3) {
            charValue = getIntValue(hexWord.charAt(hexWordIndex++));
            ipByteArray[byteIndex] = (byte) (ipByteArray[byteIndex] | (charValue << 4));
        }

        // low order 4 bits of the first byte
        if (hexWordLength > 2) {
            charValue = getIntValue(hexWord.charAt(hexWordIndex++));
            ipByteArray[byteIndex] = (byte) (ipByteArray[byteIndex] | charValue);
        }

        // high order 4 bits of second byte
        if (hexWordLength > 1) {
            charValue = getIntValue(hexWord.charAt(hexWordIndex++));
            ipByteArray[byteIndex + 1] = (byte) (ipByteArray[byteIndex + 1] | (charValue << 4));
        }

        // low order 4 bits of the first byte
        charValue = getIntValue(hexWord.charAt(hexWordIndex));
        ipByteArray[byteIndex + 1] = (byte) (ipByteArray[byteIndex + 1] | charValue & 15);
    }

    static int getIntValue(char c) {

        switch (c) {
        case '0':
            return 0;
        case '1':
            return 1;
        case '2':
            return 2;
        case '3':
            return 3;
        case '4':
            return 4;
        case '5':
            return 5;
        case '6':
            return 6;
        case '7':
            return 7;
        case '8':
            return 8;
        case '9':
            return 9;
        }

        c = Character.toLowerCase(c);
        switch (c) {
        case 'a':
            return 10;
        case 'b':
            return 11;
        case 'c':
            return 12;
        case 'd':
            return 13;
        case 'e':
            return 14;
        case 'f':
            return 15;
        }
        return 0;
    }

    private static boolean isIPv4MappedAddress(byte ipAddress[]) {

        // Check if the address matches ::FFFF:d.d.d.d
        // The first 10 bytes are 0. The next to are -1 (FF).
        // The last 4 bytes are varied.
        for (int i = 0; i < 10; i++) {
            if (ipAddress[i] != 0) {
                return false;
            }
        }

        if (ipAddress[10] != -1 || ipAddress[11] != -1) {
            return false;
        }

        return true;

    }

    /**
     * Takes the byte array and creates an integer out of four bytes starting at
     * start as the high-order byte. This method makes no checks on the validity
     * of the parameters.
     */
    public static int bytesToInt(byte bytes[], int start) {
        // First mask the byte with 255, as when a negative
        // signed byte converts to an integer, it has bits
        // on in the first 3 bytes, we are only concerned
        // about the right-most 8 bits.
        // Then shift the rightmost byte to align with its
        // position in the integer.
        int value = ((bytes[start + 3] & 255))
                | ((bytes[start + 2] & 255) << 8)
                | ((bytes[start + 1] & 255) << 16)
                | ((bytes[start] & 255) << 24);
        return value;
    }

    public static String addressToString(int value) {
        return ((value >> 24) & 0xff) + "." + ((value >> 16) & 0xff) + "."
                + ((value >> 8) & 0xff) + "." + (value & 0xff);
    }

    // BEGIN android-added
    // copied from a newer version of harmony
    public static boolean isIP6AddressInFullForm(String ipAddress) {
        if (isValidIP6Address(ipAddress)) {
            int doubleColonIndex = ipAddress.indexOf("::");
            if (doubleColonIndex >= 0) {
                // Simplified form which contains ::
                return false;
            }
            return true;
        }
        return false;
    }
    // END android-added

    public static boolean isValidIP6Address(String ipAddress) {
        int length = ipAddress.length();
        boolean doubleColon = false;
        int numberOfColons = 0;
        int numberOfPeriods = 0;
        int numberOfPercent = 0;
        String word = "";
        char c = 0;
        char prevChar = 0;
        int offset = 0; // offset for [] IP addresses

        if (length < 2) {
            return false;
        }

        for (int i = 0; i < length; i++) {
            prevChar = c;
            c = ipAddress.charAt(i);
            switch (c) {

            // case for an open bracket [x:x:x:...x]
            case '[':
                if (i != 0) {
                    return false; // must be first character
                }
                if (ipAddress.charAt(length - 1) != ']') {
                    return false; // must have a close ]
                }
                offset = 1;
                if (length < 4) {
                    return false;
                }
                break;

            // case for a closed bracket at end of IP [x:x:x:...x]
            case ']':
                if (i != length - 1) {
                    return false; // must be last character
                }
                if (ipAddress.charAt(0) != '[') {
                    return false; // must have a open [
                }
                break;

            // case for the last 32-bits represented as IPv4 x:x:x:x:x:x:d.d.d.d
            case '.':
                numberOfPeriods++;
                if (numberOfPeriods > 3) {
                    return false;
                }
                if (!isValidIP4Word(word)) {
                    return false;
                }
                if (numberOfColons != 6 && !doubleColon) {
                    return false;
                }
                // a special case ::1:2:3:4:5:d.d.d.d allows 7 colons with an
                // IPv4 ending, otherwise 7 :'s is bad
                if (numberOfColons == 7 && ipAddress.charAt(0 + offset) != ':'
                        && ipAddress.charAt(1 + offset) != ':') {
                    return false;
                }
                word = "";
                break;

            case ':':
                numberOfColons++;
                if (numberOfColons > 7) {
                    return false;
                }
                if (numberOfPeriods > 0) {
                    return false;
                }
                if (prevChar == ':') {
                    if (doubleColon) {
                        return false;
                    }
                    doubleColon = true;
                }
                word = "";
                break;
            case '%':
                if (numberOfColons == 0) {
                    return false;
                }
                numberOfPercent++;

                // validate that the stuff after the % is valid
                if ((i + 1) >= length) {
                    // in this case the percent is there but no number is
                    // available
                    return false;
                }
                try {
                    Integer.parseInt(ipAddress.substring(i + 1));
                } catch (NumberFormatException e) {
                    // right now we just support an integer after the % so if
                    // this is not
                    // what is there then return
                    return false;
                }
                break;

            default:
                if (numberOfPercent == 0) {
                    if (word.length() > 3) {
                        return false;
                    }
                    if (!isValidHexChar(c)) {
                        return false;
                    }
                }
                word += c;
            }
        }

        // Check if we have an IPv4 ending
        if (numberOfPeriods > 0) {
            if (numberOfPeriods != 3 || !isValidIP4Word(word)) {
                return false;
            }
        } else {
            // If we're at then end and we haven't had 7 colons then there is a
            // problem unless we encountered a doubleColon
            if (numberOfColons != 7 && !doubleColon) {
                return false;
            }

            // If we have an empty word at the end, it means we ended in either
            // a : or a .
            // If we did not end in :: then this is invalid
            if (numberOfPercent == 0) {
                if (word == "" && ipAddress.charAt(length - 1 - offset) == ':'
                        && ipAddress.charAt(length - 2 - offset) != ':') {
                    return false;
                }
            }
        }

        return true;
    }

    public static boolean isValidIP4Word(String word) {
        char c;
        if (word.length() < 1 || word.length() > 3) {
            return false;
        }
        for (int i = 0; i < word.length(); i++) {
            c = word.charAt(i);
            if (!(c >= '0' && c <= '9')) {
                return false;
            }
        }
        if (Integer.parseInt(word) > 255) {
            return false;
        }
        return true;
    }

    static boolean isValidHexChar(char c) {

        return (c >= '0' && c <= '9') || (c >= 'A' && c <= 'F')
                || (c >= 'a' && c <= 'f');
    }

    /**
     * Takes a string and parses it to see if it is a valid IPV4 address.
     * 
     * @return true, if the string represents an IPV4 address in dotted
     *         notation, false otherwise
     */
    public static boolean isValidIPV4Address(String value) {
        // BEGIN android-changed
        // general test
        if (!value.matches("\\p{Digit}+(\\.\\p{Digit}+)*")) {
            return false;
        }

        String[] parts = value.split("\\.");
        int length = parts.length;
        if (length < 1 || length > 4) {
            return false;
        }

        if (length == 1) {
            // One part decimal numeric address
            long longValue = Long.parseLong(parts[0]);
            return longValue <= 0xFFFFFFFFL;
        } else {
            // Test each part for inclusion in the correct range
            for (int i = 0; i < length; i++) {
                // For two part addresses, the second part expresses
                // a 24-bit quantity; for three part addresses, the third
                // part expresses a 16-bit quantity.
                int max = 0xff;
                if ((length == 2) && (i == 1)) {
                    max = 0xffffff;
                } else if ((length == 3) && (i == 2)) {
                    max = 0xffff;
                }
                if (Integer.parseInt(parts[i]) > max) {
                    return false;
                }
            }
            return true;
        }
        // END android-changed
    }

}
