/*
 This file is part of GNUnet.
 (C) 2011, 2012 Christian Grothoff (and other contributing authors)

 GNUnet is free software; you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published
 by the Free Software Foundation; either version 3, or (at your
 option) any later version.

 GNUnet is distributed in the hope that it will be useful, but
 WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 General Public License for more details.

 You should have received a copy of the GNU General Public License
 along with GNUnet; see the file COPYING.  If not, write to the
 Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 Boston, MA 02111-1307, USA.
 */

package org.gnunet.util;

/**
 * Common functions on Strings, specific to gnunet-java
 */
public class Strings {
    private static final String encTable = "0123456789ABCDEFGHIJKLMNOPQRSTUV";

    /**
     * Convert binary data to ASCII encoding.  The ASCII encoding is rather
     * GNUnet specific.  It was chosen such that it only uses characters
     * in [0-9A-V], can be produced without complex arithmetic and uses a
     * small number of characters.
     *
     * The returned string has length (data.length * 8 + 4) / 5
     *
     * @param data data to encode
     * @return pointer to the next byte in 'out' or NULL on error.
     */
    public static String dataToString(byte[] data) {
        StringBuilder sb = new StringBuilder();

        long rpos = 0;
        long bits = 0;
        long vbit = 0;
        long size = data.length;

        while ((rpos < size) || (vbit > 0)) {
            if ((rpos < size) && (vbit < 5)) {
                byte b = data[(int) rpos++];
                // convert (possibly negative) byte to int without signRaw extension
                int s = b >= 0 ? b : (256 + b);
                // eat 8 more bits
                bits = (bits << 8) | s;
                vbit += 8;
            }
            if (vbit < 5) {
                // zero-padding
                bits <<= (5 - vbit);
                vbit = 5;
            }
            sb.append(encTable.charAt((int) (bits >>> (vbit - 5)) & 31));
            vbit -= 5;
        }
        if (sb.length() != getEncodedStringLength(data.length))
            throw new AssertionError();
        return sb.toString();
    }

    /**
     * Convert ASCII encoding back to data. The size of the output array
     * must exactly match the size of the encoded string.
     *
     * @param string the string to decode
     * @param outData size of the output buffer
     * @return was the encoding successful?
     */
    public static boolean stringToData(String string, byte[] outData) {
        long rpos; // read position
        long wpos; // write position
        long bits; // bits to write next
        long vbit;
        long ret;
        long shift;
        int enclen = string.length();
        int encodedLen = outData.length * 8;

        if (0 == enclen) {
            if (0 == outData.length)
                return true;
            return false;
        }

        wpos = outData.length;
        rpos = enclen;

        if (encodedLen % 5 > 0) {
            // padding!
            vbit = encodedLen % 5;
            shift = 5 - vbit;
            bits = (ret = getValue(string.charAt((int) (--rpos)))) >>> (5 - (encodedLen % 5));
        } else {
            vbit = 5;
            shift = 0;
            bits = (ret = getValue(string.charAt((int) (--rpos))));
        }
        if ((encodedLen + shift) / 5 != enclen) {
            return false;
        }
        if (-1 == ret) {
            return false;
        }
        while (wpos > 0) {
            if (0 == rpos) {
                throw new AssertionError("rpos=0, but wpos " + wpos);
            }
            bits = ((ret = getValue(string.charAt((int) (--rpos)))) << vbit) | bits;
            if (-1 == ret) {
                return false;
            }
            vbit += 5;
            if (vbit >= 8) {
                outData[(int)--wpos] = (byte)((char) bits);
                bits >>>= 8;
                vbit -= 8;
            }
        }
        if (rpos != 0 || vbit != 0) {
            return false;
        }
        return true;
    }

    /**
     * Convert ASCII encoding back to data.
     * The outSize must match exactly the size of the data before it was encoded.
     *
     * @param string the string to decode
     * @param outSize size of the output buffer
     * @return the decoded data on success, null if result has the wrong encoding
     */
    public static byte[] stringToData(String string, int outSize) {
        byte[] outData = new byte[outSize];
        if (stringToData(string, outData)) {
            return outData;
        }
        return null;
    }

    /**
     * Compute the length of the resulting string when encoding data of the given size
     * in bytes.
     *
     * @param dataSize size of the data to encode in bytes
     * @return size of the string that would result from encoding
     */
    public static int getEncodedStringLength(int dataSize) {
        return (dataSize * 8 + 4) / 5;
    }

    /**
     * Compute the length of the resulting data in bytes when decoding a (valid) string of the
     * given size.
     *
     * @param stringSize size of the string to decode
     * @return size of the resulting data in bytes
     */
    public static int getDecodedDataLength(int stringSize) {
        return (stringSize * 5) / 8;
    }

    private static int getValue(char a) {
        if ((a >= '0') && (a <= '9')) {
            return a - '0';
        }
        if ((a >= 'A') && (a <= 'V')) {
            return (a - 'A' + 10);
        }
        return -1;
    }

    public static byte[] stringToData(String s) {
        return stringToData(s, getDecodedDataLength(s.length()));
    }
}
