//
//  ========================================================================
//  Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
//  ------------------------------------------------------------------------
//  All rights reserved. This program and the accompanying materials
//  are made available under the terms of the Eclipse Public License v1.0
//  and Apache License v2.0 which accompanies this distribution.
//
//      The Eclipse Public License is available at
//      http://www.eclipse.org/legal/epl-v10.html
//
//      The Apache License v2.0 is available at
//      http://www.opensource.org/licenses/apache2.0.php
//
//  You may elect to redistribute this code under either of these licenses.
//  ========================================================================
//

package org.eclipse.jetty.util;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;


/** Fast B64 Encoder/Decoder as described in RFC 1421.
 * <p>Does not insert or interpret whitespace as described in RFC
 * 1521. If you require this you must pre/post process your data.
 * <p> Note that in a web context the usual case is to not want
 * linebreaks or other white space in the encoded output.
 *
 */
public class B64Code
{
    private static final char __pad='=';
    private static final char[] __rfc1421alphabet=
            {
                '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','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','0','1','2','3','4','5','6','7','8','9','+','/'
            };

    private static final byte[] __rfc1421nibbles;
    static
    {
        __rfc1421nibbles=new byte[256];
        for (int i=0;i<256;i++)
            __rfc1421nibbles[i]=-1;
        for (byte b=0;b<64;b++)
            __rfc1421nibbles[(byte)__rfc1421alphabet[b]]=b;
        __rfc1421nibbles[(byte)__pad]=0;
    }

    private B64Code()
    {
    }

    /**
     * Base 64 encode as described in RFC 1421.
     * <p>Does not insert whitespace as described in RFC 1521.
     * @param s String to encode.
     * @return String containing the encoded form of the input.
     */
    public static String encode(String s)
    {
        return encode(s, (Charset)null);
    }

    /**
     * Base 64 encode as described in RFC 1421.
     * <p>Does not insert whitespace as described in RFC 1521.
     * @param s String to encode.
     * @param charEncoding String representing the name of
     *        the character encoding of the provided input String.
     * @return String containing the encoded form of the input.
     */
    public static String encode(String s,String charEncoding)
    {
        byte[] bytes;
        if (charEncoding==null)
            bytes=s.getBytes(StandardCharsets.ISO_8859_1);
        else
            bytes=s.getBytes(Charset.forName(charEncoding));
        return new String(encode(bytes));
    }

    /**
     * Base 64 encode as described in RFC 1421.
     * <p>Does not insert whitespace as described in RFC 1521.
     * @param s String to encode.
     * @param charEncoding The character encoding of the provided input String.
     * @return String containing the encoded form of the input.
     */
    public static String encode(String s, Charset charEncoding)
    {
        byte[] bytes=s.getBytes(charEncoding==null ? StandardCharsets.ISO_8859_1 : charEncoding);
        return new String(encode(bytes));
    }

    /**
     * Fast Base 64 encode as described in RFC 1421.
     * <p>Does not insert whitespace as described in RFC 1521.
     * <p> Avoids creating extra copies of the input/output.
     * @param b byte array to encode.
     * @return char array containing the encoded form of the input.
     */
    public static char[] encode(byte[] b)
    {
        if (b==null)
            return null;

        int bLen=b.length;
        int cLen=((bLen+2)/3)*4;
        char c[]=new char[cLen];
        int ci=0;
        int bi=0;
        byte b0, b1, b2;
        int stop=(bLen/3)*3;
        while (bi<stop)
        {
            b0=b[bi++];
            b1=b[bi++];
            b2=b[bi++];
            c[ci++]=__rfc1421alphabet[(b0>>>2)&0x3f];
            c[ci++]=__rfc1421alphabet[(b0<<4)&0x3f|(b1>>>4)&0x0f];
            c[ci++]=__rfc1421alphabet[(b1<<2)&0x3f|(b2>>>6)&0x03];
            c[ci++]=__rfc1421alphabet[b2&0x3f];
        }

        if (bLen!=bi)
        {
            switch (bLen%3)
            {
                case 2:
                    b0=b[bi++];
                    b1=b[bi++];
                    c[ci++]=__rfc1421alphabet[(b0>>>2)&0x3f];
                    c[ci++]=__rfc1421alphabet[(b0<<4)&0x3f|(b1>>>4)&0x0f];
                    c[ci++]=__rfc1421alphabet[(b1<<2)&0x3f];
                    c[ci++]=__pad;
                    break;

                case 1:
                    b0=b[bi++];
                    c[ci++]=__rfc1421alphabet[(b0>>>2)&0x3f];
                    c[ci++]=__rfc1421alphabet[(b0<<4)&0x3f];
                    c[ci++]=__pad;
                    c[ci++]=__pad;
                    break;

                default:
                    break;
            }
        }

        return c;
    }

    /**
     * Fast Base 64 encode as described in RFC 1421 and RFC2045
     * <p>Does not insert whitespace as described in RFC 1521, unless rfc2045 is passed as true.
     * <p> Avoids creating extra copies of the input/output.
     * @param b byte array to encode.
     * @param rfc2045 If true, break lines at 76 characters with CRLF
     * @return char array containing the encoded form of the input.
     */
    public static char[] encode(byte[] b, boolean rfc2045)
    {
        if (b==null)
            return null;
        if (!rfc2045)
            return encode(b);

        int bLen=b.length;
        int cLen=((bLen+2)/3)*4;
        cLen+=2+2*(cLen/76);
        char c[]=new char[cLen];
        int ci=0;
        int bi=0;
        byte b0, b1, b2;
        int stop=(bLen/3)*3;
        int l=0;
        while (bi<stop)
        {
            b0=b[bi++];
            b1=b[bi++];
            b2=b[bi++];
            c[ci++]=__rfc1421alphabet[(b0>>>2)&0x3f];
            c[ci++]=__rfc1421alphabet[(b0<<4)&0x3f|(b1>>>4)&0x0f];
            c[ci++]=__rfc1421alphabet[(b1<<2)&0x3f|(b2>>>6)&0x03];
            c[ci++]=__rfc1421alphabet[b2&0x3f];
            l+=4;
            if (l%76==0)
            {
                c[ci++]=13;
                c[ci++]=10;
            }
        }

        if (bLen!=bi)
        {
            switch (bLen%3)
            {
                case 2:
                    b0=b[bi++];
                    b1=b[bi++];
                    c[ci++]=__rfc1421alphabet[(b0>>>2)&0x3f];
                    c[ci++]=__rfc1421alphabet[(b0<<4)&0x3f|(b1>>>4)&0x0f];
                    c[ci++]=__rfc1421alphabet[(b1<<2)&0x3f];
                    c[ci++]=__pad;
                    break;

                case 1:
                    b0=b[bi++];
                    c[ci++]=__rfc1421alphabet[(b0>>>2)&0x3f];
                    c[ci++]=__rfc1421alphabet[(b0<<4)&0x3f];
                    c[ci++]=__pad;
                    c[ci++]=__pad;
                    break;

                default:
                    break;
            }
        }

        c[ci++]=13;
        c[ci++]=10;
        return c;
    }

    /**
     * Base 64 decode as described in RFC 2045.
     * <p>Unlike {@link #decode(char[])}, extra whitespace is ignored.
     * @param encoded String to decode.
     * @param charEncoding String representing the character encoding
     *        used to map the decoded bytes into a String.
     * @return String decoded byte array.
     * @throws UnsupportedCharsetException if the encoding is not supported
     * @throws IllegalArgumentException if the input is not a valid
     *         B64 encoding.
     */
    public static String decode(String encoded,String charEncoding)
    {
        byte[] decoded=decode(encoded);
        if (charEncoding==null)
            return new String(decoded);
        return new String(decoded,Charset.forName(charEncoding));
    }

    /**
     * Base 64 decode as described in RFC 2045.
     * <p>Unlike {@link #decode(char[])}, extra whitespace is ignored.
     * @param encoded String to decode.
     * @param charEncoding Character encoding
     *        used to map the decoded bytes into a String.
     * @return String decoded byte array.
     * @throws IllegalArgumentException if the input is not a valid
     *         B64 encoding.
     */
    public static String decode(String encoded, Charset charEncoding)
    {
        byte[] decoded=decode(encoded);
        if (charEncoding==null)
            return new String(decoded);
        return new String(decoded, charEncoding);
    }

    /**
     * Fast Base 64 decode as described in RFC 1421.
     *
     * <p>Unlike other decode methods, this does not attempt to
     * cope with extra whitespace as described in RFC 1521/2045.
     * <p> Avoids creating extra copies of the input/output.
     * <p> Note this code has been flattened for performance.
     * @param b char array to decode.
     * @return byte array containing the decoded form of the input.
     * @throws IllegalArgumentException if the input is not a valid
     *         B64 encoding.
     */
    public static byte[] decode(char[] b)
    {
        if (b==null)
            return null;

        int bLen=b.length;
        if (bLen%4!=0)
            throw new IllegalArgumentException("Input block size is not 4");

        int li=bLen-1;
        while (li>=0 && b[li]==(byte)__pad)
            li--;

        if (li<0)
            return new byte[0];

        // Create result array of exact required size.
        int rLen=((li+1)*3)/4;
        byte r[]=new byte[rLen];
        int ri=0;
        int bi=0;
        int stop=(rLen/3)*3;
        byte b0,b1,b2,b3;
        try
        {
            while (ri<stop)
            {
                b0=__rfc1421nibbles[b[bi++]];
                b1=__rfc1421nibbles[b[bi++]];
                b2=__rfc1421nibbles[b[bi++]];
                b3=__rfc1421nibbles[b[bi++]];
                if (b0<0 || b1<0 || b2<0 || b3<0)
                    throw new IllegalArgumentException("Not B64 encoded");

                r[ri++]=(byte)(b0<<2|b1>>>4);
                r[ri++]=(byte)(b1<<4|b2>>>2);
                r[ri++]=(byte)(b2<<6|b3);
            }

            if (rLen!=ri)
            {
                switch (rLen%3)
                {
                    case 2:
                        b0=__rfc1421nibbles[b[bi++]];
                        b1=__rfc1421nibbles[b[bi++]];
                        b2=__rfc1421nibbles[b[bi++]];
                        if (b0<0 || b1<0 || b2<0)
                            throw new IllegalArgumentException("Not B64 encoded");
                        r[ri++]=(byte)(b0<<2|b1>>>4);
                        r[ri++]=(byte)(b1<<4|b2>>>2);
                        break;

                    case 1:
                        b0=__rfc1421nibbles[b[bi++]];
                        b1=__rfc1421nibbles[b[bi++]];
                        if (b0<0 || b1<0)
                            throw new IllegalArgumentException("Not B64 encoded");
                        r[ri++]=(byte)(b0<<2|b1>>>4);
                        break;

                    default:
                        break;
                }
            }
        }
        catch (IndexOutOfBoundsException e)
        {
            throw new IllegalArgumentException("char "+bi
                    +" was not B64 encoded");
        }

        return r;
    }

    /**
     * Base 64 decode as described in RFC 2045.
     * <p>Unlike {@link #decode(char[])}, extra whitespace is ignored.
     * @param encoded String to decode.
     * @return byte array containing the decoded form of the input.
     * @throws IllegalArgumentException if the input is not a valid
     *         B64 encoding.
     */
    public static byte[] decode(String encoded)
    {
        if (encoded==null)
            return null;

        ByteArrayOutputStream bout = new ByteArrayOutputStream(4*encoded.length()/3);        
        decode(encoded, bout);
        return bout.toByteArray();
    }
    
    /* ------------------------------------------------------------ */
    /**
     * Base 64 decode as described in RFC 2045.
     * <p>Unlike {@link #decode(char[])}, extra whitespace is ignored.
     * @param encoded String to decode.
     * @param bout stream for decoded bytes
     * @throws IllegalArgumentException if the input is not a valid
     *         B64 encoding.
     */
    static public void decode (String encoded, ByteArrayOutputStream bout)
    {
        if (encoded==null)
            return;
        
        if (bout == null)
            throw new IllegalArgumentException("No outputstream for decoded bytes");
        
        int ci=0;
        byte nibbles[] = new byte[4];
        int s=0;
  
        while (ci<encoded.length())
        {
            char c=encoded.charAt(ci++);

            if (c==__pad)
                break;

            if (Character.isWhitespace(c))
                continue;

            byte nibble=__rfc1421nibbles[c];
            if (nibble<0)
                throw new IllegalArgumentException("Not B64 encoded");

            nibbles[s++]=__rfc1421nibbles[c];

            switch(s)
            {
                case 1:
                    break;
                case 2:
                    bout.write(nibbles[0]<<2|nibbles[1]>>>4);
                    break;
                case 3:
                    bout.write(nibbles[1]<<4|nibbles[2]>>>2);
                    break;
                case 4:
                    bout.write(nibbles[2]<<6|nibbles[3]);
                    s=0;
                    break;
            }

        }

        return;
    }
    

    public static void encode(int value,Appendable buf) throws IOException
    {
        buf.append(__rfc1421alphabet[0x3f&((0xFC000000&value)>>26)]);
        buf.append(__rfc1421alphabet[0x3f&((0x03F00000&value)>>20)]);
        buf.append(__rfc1421alphabet[0x3f&((0x000FC000&value)>>14)]);
        buf.append(__rfc1421alphabet[0x3f&((0x00003F00&value)>>8)]);
        buf.append(__rfc1421alphabet[0x3f&((0x000000FC&value)>>2)]);
        buf.append(__rfc1421alphabet[0x3f&((0x00000003&value)<<4)]);
        buf.append('=');
    }

    public static void encode(long lvalue,Appendable buf) throws IOException
    {
        int value=(int)(0xFFFFFFFC&(lvalue>>32));
        buf.append(__rfc1421alphabet[0x3f&((0xFC000000&value)>>26)]);
        buf.append(__rfc1421alphabet[0x3f&((0x03F00000&value)>>20)]);
        buf.append(__rfc1421alphabet[0x3f&((0x000FC000&value)>>14)]);
        buf.append(__rfc1421alphabet[0x3f&((0x00003F00&value)>>8)]);
        buf.append(__rfc1421alphabet[0x3f&((0x000000FC&value)>>2)]);

        buf.append(__rfc1421alphabet[0x3f&((0x00000003&value)<<4) + (0xf&(int)(lvalue>>28))]);

        value=0x0FFFFFFF&(int)lvalue;
        buf.append(__rfc1421alphabet[0x3f&((0x0FC00000&value)>>22)]);
        buf.append(__rfc1421alphabet[0x3f&((0x003F0000&value)>>16)]);
        buf.append(__rfc1421alphabet[0x3f&((0x0000FC00&value)>>10)]);
        buf.append(__rfc1421alphabet[0x3f&((0x000003F0&value)>>4)]);
        buf.append(__rfc1421alphabet[0x3f&((0x0000000F&value)<<2)]);
    }
}
