package net.i2p.data;

/*
 * Released into the public domain 
 * with no warranty of any kind, either expressed or implied.  
 */

import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Locale;

/**
 * Encodes and decodes to and from Base32 notation.
 * Ref: RFC 3548
 *
 * Don't bother with '=' padding characters on encode or
 * accept them on decode (i.e. don't require 5-character groups).
 * No whitespace allowed.
 *
 * Decode accepts upper or lower case.
 * @author zzz
 * @since 0.7
 */
public class Base32 {

    //private final static Log _log = new Log(Base32.class);

    /** The 32 valid Base32 values. */
    private final static char[] ALPHABET = {'a', 'b', 'c', 'd',
                                            'e', 'f', 'g', 'h', 'i', 'j',
                                            'k', 'l', 'm', 'n', 'o', 'p',
                                            'q', 'r', 's', 't', 'u', 'v',
                                            'w', 'x', 'y', 'z',
                                            '2', '3', '4', '5', '6', '7'};

    /** 
     * Translates a Base32 value to either its 5-bit reconstruction value
     * or a negative number indicating some other meaning.
     * Allow upper or lower case.
     **/
    private final static byte[] DECODABET = {
                                             26, 27, 28, 29, 30, 31, -9, -9, // Numbers two through nine
                                             -9, -9, -9, // Decimal 58 - 60
                                             -1, // Equals sign at decimal 61
                                             -9, -9, -9, // Decimal 62 - 64
                                             0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, // Letters 'A' through 'M'
                                             13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'N' through 'Z'
                                             -9, -9, -9, -9, -9, -9, // Decimal 91 - 96
                                             0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, // Letters 'a' through 'm'
                                             13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'n' through 'z'
                                             -9, -9, -9, -9, -9 // Decimal 123 - 127
    };

    private final static byte BAD_ENCODING = -9; // Indicates error in encoding

    /** Defeats instantiation. */
    private Base32() { // nop
    }

    public static void main(String[] args) {
        if (args.length == 0) {
            help();
            return;
        }
        runApp(args);
    }

    private static void runApp(String args[]) {
        String cmd = args[0].toLowerCase(Locale.US);
        if ("encodestring".equals(cmd)) {
            System.out.println(encode(DataHelper.getUTF8(args[1])));
            return;
        }
        InputStream in = System.in;
        OutputStream out = System.out;
        try {
            if (args.length >= 3) {
                out = new FileOutputStream(args[2]);
            }
            if (args.length >= 2) {
                in = new FileInputStream(args[1]);
            }
            if ("encode".equals(cmd)) {
                encode(in, out);
                return;
            }
            if ("decode".equals(cmd)) {
                decode(in, out);
                return;
            }
        } catch (IOException ioe) {
            ioe.printStackTrace(System.err);
        } finally {
            try { in.close(); } catch (IOException e) {}
            try { out.close(); } catch (IOException e) {}
        }
    }

    private static byte[] read(InputStream in) throws IOException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream(64);
        DataHelper.copy(in, baos);
        return baos.toByteArray();
    }

    private static void encode(InputStream in, OutputStream out) throws IOException {
        String encoded = encode(read(in));
        for (int i = 0; i < encoded.length(); i++)
            out.write((byte)(encoded.charAt(i) & 0xFF));
    }

    private static void decode(InputStream in, OutputStream out) throws IOException {
        byte decoded[] = decode(DataHelper.getUTF8(read(in)));
        if (decoded == null) {
            System.err.println("FAIL");
            return;
        }
        out.write(decoded);
    }

    private static void help() {
        System.out.println("Syntax: Base32 encode <inFile> <outFile>");
        System.out.println("or    : Base32 encode <inFile>");
        System.out.println("or    : Base32 encodestring <string>");
        System.out.println("or    : Base32 encode");
        System.out.println("or    : Base32 decode <inFile> <outFile>");
        System.out.println("or    : Base32 decode <inFile>");
        System.out.println("or    : Base32 decode");
    }

    /**
     *  Returns lower case.
     *  Does not add trailing '='.
     *
     *  @param source if null will return ""
     */
    public static String encode(String source) {
        return (source != null ? encode(DataHelper.getUTF8(source)) : "");
    }

    /**
     * Returns lower case.
     * Does not add trailing '='.
     *
     * @param source The data to convert non-null
     */
    public static String encode(byte[] source) {
        StringBuilder buf = new StringBuilder((source.length + 7) * 8 / 5);
        encodeBytes(source, buf);
        return buf.toString();
    }

    private final static byte[] emask = { (byte) 0x1f,
                                          (byte) 0x01, (byte) 0x03, (byte) 0x07, (byte) 0x0f };
    /**
     * Encodes a byte array into Base32 notation.
     *
     * @param source The data to convert non-null
     */
    private static void encodeBytes(byte[] source, StringBuilder out) {
        int usedbits = 0;
        for (int i = 0; i < source.length; ) {
             int fivebits;
             if (usedbits < 3) {
                 fivebits = (source[i] >> (3 - usedbits)) & 0x1f;
                 usedbits += 5;
             } else if (usedbits == 3) {
                 fivebits = source[i++] & 0x1f;
                 usedbits = 0;
             } else {
                 fivebits = (source[i++] << (usedbits - 3)) & 0x1f;
                 if (i < source.length) {
                     usedbits -= 3;
                     fivebits |= (source[i] >> (8 - usedbits)) & emask[usedbits];
                 }
             }
             out.append(ALPHABET[fivebits]);
        }
    }

    /**
     * Decodes data from Base32 notation and
     * returns it as a string.
     * Case-insensitive.
     * Does not allow trailing '='.
     *
     * @param s the string to decode, if null returns null
     * @return The data as a string or null on failure
     */
    public static String decodeToString(String s) {
        byte[] b = decode(s);
        if (b == null)
            return null;
        return DataHelper.getUTF8(b);
    }

    /**
     * Case-insensitive.
     * Does not allow trailing '='.
     *
     * @param s non-null
     * @return decoded data, null on error
     */
    public static byte[] decode(String s) {
        return decode(DataHelper.getASCII(s));
    }

    private final static byte[] dmask = { (byte) 0xf8, (byte) 0x7c, (byte) 0x3e, (byte) 0x1f,
                                          (byte) 0x0f, (byte) 0x07, (byte) 0x03, (byte) 0x01 };
    /**
     * Decodes Base32 content in byte array format and returns
     * the decoded byte array.
     *
     * @param source The Base32 encoded data non-null
     * @return decoded data, null on error
     */
    private static byte[] decode(byte[] source) {
        int len58;
        if (source.length <= 1)
            len58 = source.length;
        else
            len58 = source.length * 5 / 8;
        byte[] outBuff = new byte[len58];
        int outBuffPosn = 0;

        int usedbits = 0;
        for (int i = 0; i < source.length; i++) {
            int fivebits;
            if ((source[i] & 0x80) != 0 || source[i] < '2' || source[i] > 'z')
                fivebits = BAD_ENCODING;
            else
                fivebits = DECODABET[source[i] - '2'];

            if (fivebits >= 0) {
                 if (outBuffPosn >= len58)
                     return null;
                 if (usedbits == 0) {
                     outBuff[outBuffPosn] = (byte) ((fivebits << 3) & 0xf8);
                     usedbits = 5;
                 } else if (usedbits < 3) {
                     outBuff[outBuffPosn] |= (fivebits << (3 - usedbits)) & dmask[usedbits];
                     usedbits += 5;
                 } else if (usedbits == 3) {
                     outBuff[outBuffPosn++] |= fivebits;
                     usedbits = 0;
                 } else {
                     outBuff[outBuffPosn++] |= (fivebits >> (usedbits - 3)) & dmask[usedbits];
                     byte next = (byte) (fivebits << (11 - usedbits));
                     if (outBuffPosn < len58) {
                         outBuff[outBuffPosn] = next;
                         usedbits -= 3;
                     } else if (next != 0) {
                       //_log.warn("Extra data at the end: " + next + "(decimal)");
                       return null;
                     }
                 }
            } else {
                //_log.warn("Bad Base32 input character at " + i + ": " + source[i] + "(decimal)");
                return null;
            }
        }
        return outBuff;
    }
}
