/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
 * or http://forgerock.org/license/CDDLv1.0.html.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at legal-notices/CDDLv1_0.txt.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *      Copyright 2006-2009 Sun Microsystems, Inc.
 *      Portions Copyright 2014-2015 ForgeRock AS.
 */
package org.opends.server.util;

import static org.forgerock.util.Reject.*;
import static org.opends.messages.ToolMessages.*;
import static org.opends.messages.UtilityMessages.*;
import static org.opends.server.util.StaticUtils.*;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.StringTokenizer;

import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.opendj.ldap.ByteSequence;
import org.opends.server.core.DirectoryServer.DirectoryServerVersionHandler;
import org.opends.server.types.NullOutputStream;

import com.forgerock.opendj.cli.ArgumentException;
import com.forgerock.opendj.cli.BooleanArgument;
import com.forgerock.opendj.cli.CommonArguments;
import com.forgerock.opendj.cli.StringArgument;
import com.forgerock.opendj.cli.SubCommand;
import com.forgerock.opendj.cli.SubCommandArgumentParser;

/**
 * This class provides methods for performing base64 encoding and decoding.
 * Base64 is a mechanism for encoding binary data in ASCII form by converting
 * sets of three bytes with eight significant bits each to sets of four bytes
 * with six significant bits each.
 */
@org.opends.server.types.PublicAPI(
     stability=org.opends.server.types.StabilityLevel.UNCOMMITTED,
     mayInstantiate=false,
     mayExtend=false,
     mayInvoke=true)
public final class Base64
{
  /** The set of characters that may be used in base64-encoded values. */
  private static final char[] BASE64_ALPHABET =
      "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".toCharArray();

  /** Prevent instance creation. */
  private Base64() {
    // No implementation required.
  }

  /**
   * Encodes the provided raw data using base64.
   *
   * @param  rawData  The raw data to encode.  It must not be <CODE>null</CODE>.
   *
   * @return  The base64-encoded representation of the provided raw data.
   */
  public static String encode(byte[] rawData)
  {
    ifNull(rawData);


    StringBuilder buffer = new StringBuilder(4 * rawData.length / 3);

    int pos = 0;
    int iterations = rawData.length / 3;
    for (int i=0; i < iterations; i++)
    {
      int value = ((rawData[pos++] & 0xFF) << 16) |
                  ((rawData[pos++] & 0xFF) <<  8) | (rawData[pos++] & 0xFF);

      buffer.append(BASE64_ALPHABET[(value >>> 18) & 0x3F]);
      buffer.append(BASE64_ALPHABET[(value >>> 12) & 0x3F]);
      buffer.append(BASE64_ALPHABET[(value >>>  6) & 0x3F]);
      buffer.append(BASE64_ALPHABET[value & 0x3F]);
    }


    switch (rawData.length % 3)
    {
      case 1:
        buffer.append(BASE64_ALPHABET[(rawData[pos] >>> 2) & 0x3F]);
        buffer.append(BASE64_ALPHABET[(rawData[pos] <<  4) & 0x3F]);
        buffer.append("==");
        break;
      case 2:
        int value = ((rawData[pos++] & 0xFF) << 8) | (rawData[pos] & 0xFF);
        buffer.append(BASE64_ALPHABET[(value >>> 10) & 0x3F]);
        buffer.append(BASE64_ALPHABET[(value >>>  4) & 0x3F]);
        buffer.append(BASE64_ALPHABET[(value <<   2) & 0x3F]);
        buffer.append("=");
        break;
    }

    return buffer.toString();
  }

  /**
   * Encodes the provided raw data using base64.
   *
   * @param  rawData  The raw data to encode.  It must not be <CODE>null</CODE>.
   *
   * @return  The base64-encoded representation of the provided raw data.
   */
  public static String encode(ByteSequence rawData)
  {
    ifNull(rawData);


    StringBuilder buffer = new StringBuilder(4 * rawData.length() / 3);

    int pos = 0;
    int iterations = rawData.length() / 3;
    for (int i=0; i < iterations; i++)
    {
      int value = ((rawData.byteAt(pos++) & 0xFF) << 16) |
                  ((rawData.byteAt(pos++) & 0xFF) <<  8) |
          (rawData.byteAt(pos++) & 0xFF);

      buffer.append(BASE64_ALPHABET[(value >>> 18) & 0x3F]);
      buffer.append(BASE64_ALPHABET[(value >>> 12) & 0x3F]);
      buffer.append(BASE64_ALPHABET[(value >>>  6) & 0x3F]);
      buffer.append(BASE64_ALPHABET[value & 0x3F]);
    }


    switch (rawData.length() % 3)
    {
      case 1:
        buffer.append(BASE64_ALPHABET[(rawData.byteAt(pos) >>> 2) & 0x3F]);
        buffer.append(BASE64_ALPHABET[(rawData.byteAt(pos) <<  4) & 0x3F]);
        buffer.append("==");
        break;
      case 2:
        int value = ((rawData.byteAt(pos++) & 0xFF) << 8) |
            (rawData.byteAt(pos) & 0xFF);
        buffer.append(BASE64_ALPHABET[(value >>> 10) & 0x3F]);
        buffer.append(BASE64_ALPHABET[(value >>>  4) & 0x3F]);
        buffer.append(BASE64_ALPHABET[(value <<   2) & 0x3F]);
        buffer.append("=");
        break;
    }

    return buffer.toString();
  }



  /**
   * Decodes the provided set of base64-encoded data.
   *
   * @param  encodedData  The base64-encoded data to decode.  It must not be
   *                      <CODE>null</CODE>.
   *
   * @return  The decoded raw data.
   *
   * @throws  ParseException  If a problem occurs while attempting to decode the
   *                          provided data.
   */
  public static byte[] decode(String encodedData)
         throws ParseException
  {
    ifNull(encodedData);


    // The encoded value must have  length that is a multiple of four bytes.
    int length = encodedData.length();
    if (length % 4 != 0)
    {
      LocalizableMessage message = ERR_BASE64_DECODE_INVALID_LENGTH.get(encodedData);
      throw new ParseException(message.toString(), 0);
    }


    ByteBuffer buffer = ByteBuffer.allocate(length);
    for (int i=0; i < length; i += 4)
    {
      boolean append = true;
      int     value  = 0;

      for (int j=0; j < 4; j++)
      {
        switch (encodedData.charAt(i+j))
        {
          case 'A':
            value <<= 6;
            break;
          case 'B':
            value = (value << 6) | 0x01;
            break;
          case 'C':
            value = (value << 6) | 0x02;
            break;
          case 'D':
            value = (value << 6) | 0x03;
            break;
          case 'E':
            value = (value << 6) | 0x04;
            break;
          case 'F':
            value = (value << 6) | 0x05;
            break;
          case 'G':
            value = (value << 6) | 0x06;
            break;
          case 'H':
            value = (value << 6) | 0x07;
            break;
          case 'I':
            value = (value << 6) | 0x08;
            break;
          case 'J':
            value = (value << 6) | 0x09;
            break;
          case 'K':
            value = (value << 6) | 0x0A;
            break;
          case 'L':
            value = (value << 6) | 0x0B;
            break;
          case 'M':
            value = (value << 6) | 0x0C;
            break;
          case 'N':
            value = (value << 6) | 0x0D;
            break;
          case 'O':
            value = (value << 6) | 0x0E;
            break;
          case 'P':
            value = (value << 6) | 0x0F;
            break;
          case 'Q':
            value = (value << 6) | 0x10;
            break;
          case 'R':
            value = (value << 6) | 0x11;
            break;
          case 'S':
            value = (value << 6) | 0x12;
            break;
          case 'T':
            value = (value << 6) | 0x13;
            break;
          case 'U':
            value = (value << 6) | 0x14;
            break;
          case 'V':
            value = (value << 6) | 0x15;
            break;
          case 'W':
            value = (value << 6) | 0x16;
            break;
          case 'X':
            value = (value << 6) | 0x17;
            break;
          case 'Y':
            value = (value << 6) | 0x18;
            break;
          case 'Z':
            value = (value << 6) | 0x19;
            break;
          case 'a':
            value = (value << 6) | 0x1A;
            break;
          case 'b':
            value = (value << 6) | 0x1B;
            break;
          case 'c':
            value = (value << 6) | 0x1C;
            break;
          case 'd':
            value = (value << 6) | 0x1D;
            break;
          case 'e':
            value = (value << 6) | 0x1E;
            break;
          case 'f':
            value = (value << 6) | 0x1F;
            break;
          case 'g':
            value = (value << 6) | 0x20;
            break;
          case 'h':
            value = (value << 6) | 0x21;
            break;
          case 'i':
            value = (value << 6) | 0x22;
            break;
          case 'j':
            value = (value << 6) | 0x23;
            break;
          case 'k':
            value = (value << 6) | 0x24;
            break;
          case 'l':
            value = (value << 6) | 0x25;
            break;
          case 'm':
            value = (value << 6) | 0x26;
            break;
          case 'n':
            value = (value << 6) | 0x27;
            break;
          case 'o':
            value = (value << 6) | 0x28;
            break;
          case 'p':
            value = (value << 6) | 0x29;
            break;
          case 'q':
            value = (value << 6) | 0x2A;
            break;
          case 'r':
            value = (value << 6) | 0x2B;
            break;
          case 's':
            value = (value << 6) | 0x2C;
            break;
          case 't':
            value = (value << 6) | 0x2D;
            break;
          case 'u':
            value = (value << 6) | 0x2E;
            break;
          case 'v':
            value = (value << 6) | 0x2F;
            break;
          case 'w':
            value = (value << 6) | 0x30;
            break;
          case 'x':
            value = (value << 6) | 0x31;
            break;
          case 'y':
            value = (value << 6) | 0x32;
            break;
          case 'z':
            value = (value << 6) | 0x33;
            break;
          case '0':
            value = (value << 6) | 0x34;
            break;
          case '1':
            value = (value << 6) | 0x35;
            break;
          case '2':
            value = (value << 6) | 0x36;
            break;
          case '3':
            value = (value << 6) | 0x37;
            break;
          case '4':
            value = (value << 6) | 0x38;
            break;
          case '5':
            value = (value << 6) | 0x39;
            break;
          case '6':
            value = (value << 6) | 0x3A;
            break;
          case '7':
            value = (value << 6) | 0x3B;
            break;
          case '8':
            value = (value << 6) | 0x3C;
            break;
          case '9':
            value = (value << 6) | 0x3D;
            break;
          case '+':
            value = (value << 6) | 0x3E;
            break;
          case '/':
            value = (value << 6) | 0x3F;
            break;
          case '=':
            append = false;
            switch (j)
            {
              case 2:
                buffer.put((byte) ((value >>> 4) & 0xFF));
                break;
              case 3:
                buffer.put((byte) ((value >>> 10) & 0xFF));
                buffer.put((byte) ((value >>>  2) & 0xFF));
                break;
            }
            break;
          default:
            LocalizableMessage message = ERR_BASE64_DECODE_INVALID_CHARACTER.get(
                encodedData, encodedData.charAt(i+j));
            throw new ParseException(message.toString(), i+j);
        }


        if (! append)
        {
          break;
        }
      }


      if (append)
      {
        buffer.put((byte) ((value >>> 16) & 0xFF));
        buffer.put((byte) ((value >>>  8) & 0xFF));
        buffer.put((byte) (value & 0xFF));
      }
      else
      {
        break;
      }
    }


    buffer.flip();
    byte[] returnArray = new byte[buffer.limit()];
    buffer.get(returnArray);
    return returnArray;
  }



  /**
   * Provide a command-line utility that may be used to base64-encode and
   * decode strings and file contents.
   *
   * @param  args  The command-line arguments provided to this program.
   */
  public static void main(String[] args)
  {
    LocalizableMessage description = INFO_BASE64_TOOL_DESCRIPTION.get();
    SubCommandArgumentParser argParser =
         new SubCommandArgumentParser(Base64.class.getName(), description,
                                      false);
    argParser.setShortToolDescription(REF_SHORT_DESC_BASE64.get());
    argParser.setVersionHandler(new DirectoryServerVersionHandler());

    BooleanArgument showUsage        = null;
    StringArgument  encodedData      = null;
    StringArgument  encodedFile      = null;
    StringArgument  rawData          = null;
    StringArgument  rawFile          = null;
    StringArgument  toEncodedFile    = null;
    StringArgument  toRawFile        = null;
    SubCommand      decodeSubCommand = null;
    SubCommand      encodeSubCommand = null;

    try
    {
      decodeSubCommand = new SubCommand(argParser, "decode",
                                        INFO_BASE64_DECODE_DESCRIPTION.get());

      encodeSubCommand = new SubCommand(argParser, "encode",
                                        INFO_BASE64_ENCODE_DESCRIPTION.get());


      encodedData = new StringArgument("encodeddata", 'd', "encodedData", false,
                             false, true, INFO_DATA_PLACEHOLDER.get(), null,
                             null,
                             INFO_BASE64_ENCODED_DATA_DESCRIPTION.get());
      decodeSubCommand.addArgument(encodedData);


      encodedFile = new StringArgument("encodedfile", 'f', "encodedDataFile",
                             false, false, true, INFO_PATH_PLACEHOLDER.get(),
                             null, null,
                             INFO_BASE64_ENCODED_FILE_DESCRIPTION.get());
      decodeSubCommand.addArgument(encodedFile);


      toRawFile = new StringArgument("torawfile", 'o', "toRawFile", false,
                                     false, true, INFO_PATH_PLACEHOLDER.get(),
                                     null, null,
                                     INFO_BASE64_TO_RAW_FILE_DESCRIPTION.get());
      decodeSubCommand.addArgument(toRawFile);


      rawData = new StringArgument("rawdata", 'd', "rawData", false, false,
                                   true, INFO_DATA_PLACEHOLDER.get(), null,
                                   null,
                                   INFO_BASE64_RAW_DATA_DESCRIPTION.get());
      encodeSubCommand.addArgument(rawData);


      rawFile = new StringArgument("rawfile", 'f', "rawDataFile", false, false,
                                   true, INFO_PATH_PLACEHOLDER.get(), null,
                                   null,
                                   INFO_BASE64_RAW_FILE_DESCRIPTION.get());
      encodeSubCommand.addArgument(rawFile);


      toEncodedFile = new StringArgument("toencodedfile", 'o', "toEncodedFile",
                               false, false, true, INFO_PATH_PLACEHOLDER.get(),
                               null, null,
                               INFO_BASE64_TO_ENCODED_FILE_DESCRIPTION.get());
      encodeSubCommand.addArgument(toEncodedFile);


      ArrayList<SubCommand> subCommandList = new ArrayList<>(2);
      subCommandList.add(decodeSubCommand);
      subCommandList.add(encodeSubCommand);


      showUsage = CommonArguments.getShowUsage();
      argParser.addGlobalArgument(showUsage);
      argParser.setUsageGroupArgument(showUsage, subCommandList);
      argParser.setUsageArgument(showUsage, NullOutputStream.printStream());
    }
    catch (ArgumentException ae)
    {
      System.err.println(ERR_CANNOT_INITIALIZE_ARGS.get(ae.getMessage()));
      System.exit(1);
    }

    try
    {
      argParser.parseArguments(args);
    }
    catch (ArgumentException ae)
    {
      argParser.displayMessageAndUsageReference(System.err, ERR_ERROR_PARSING_ARGS.get(ae.getMessage()));
      System.exit(1);
    }

    SubCommand subCommand = argParser.getSubCommand();
    if (argParser.isUsageArgumentPresent())
    {
      if (subCommand == null)
      {
        System.out.println(argParser.getUsage());
      }
      else
      {
        final StringBuilder messageBuilder = new StringBuilder();
        argParser.getSubCommandUsage(messageBuilder, subCommand);
        System.out.println(messageBuilder.toString());
      }

      return;
    }

    if (argParser.isVersionArgumentPresent())
    {
      // version has already been printed
      System.exit(0);
    }

    if (subCommand == null)
    {
      System.err.println(argParser.getUsage());
      System.exit(1);
    }
    if (subCommand.getName().equals(encodeSubCommand.getName()))
    {
      byte[] dataToEncode = null;
      if (rawData.isPresent())
      {
        try
        {
          dataToEncode = rawData.getValue().getBytes("UTF-8");
        }
        catch(UnsupportedEncodingException ex)
        {
          System.err.println(ERR_UNEXPECTED.get(ex));
          System.exit(1);
        }
      }
      else
      {
        try
        {
          boolean shouldClose;
          InputStream inputStream;
          if (rawFile.isPresent())
          {
            inputStream = new FileInputStream(rawFile.getValue());
            shouldClose = true;
          }
          else
          {
            inputStream = System.in;
            shouldClose = false;
          }

          ByteArrayOutputStream baos = new ByteArrayOutputStream();
          byte[] buffer = new byte[8192];
          while (true)
          {
            int bytesRead = inputStream.read(buffer);
            if (bytesRead < 0)
            {
              break;
            }
            else
            {
              baos.write(buffer, 0, bytesRead);
            }
          }

          if (shouldClose)
          {
            inputStream.close();
          }

          dataToEncode = baos.toByteArray();
        }
        catch (Exception e)
        {
          System.err.println(ERR_BASE64_CANNOT_READ_RAW_DATA.get(
                                  getExceptionMessage(e)));
          System.exit(1);
        }
      }

      String base64Data = encode(dataToEncode);
      if (toEncodedFile.isPresent())
      {
        try
        {
          BufferedWriter writer =
               new BufferedWriter(new FileWriter(toEncodedFile.getValue()));
          writer.write(base64Data);
          writer.newLine();
          writer.close();
        }
        catch (Exception e)
        {
          System.err.println(ERR_BASE64_CANNOT_WRITE_ENCODED_DATA.get(
                                  getExceptionMessage(e)));
          System.exit(1);
        }
      }
      else
      {
        System.out.println(base64Data);
      }
    }
    else if (subCommand.getName().equals(decodeSubCommand.getName()))
    {
      String dataToDecode = null;
      if (encodedData.isPresent())
      {
        dataToDecode = encodedData.getValue();
      }
      else
      {
        try
        {
          boolean shouldClose;
          BufferedReader reader;
          if (encodedFile.isPresent())
          {
            reader = new BufferedReader(new FileReader(encodedFile.getValue()));
            shouldClose = true;
          }
          else
          {
            reader = new BufferedReader(new InputStreamReader(System.in));
            shouldClose = false;
          }

          StringBuilder buffer = new StringBuilder();
          while (true)
          {
            String line = reader.readLine();
            if (line == null)
            {
              break;
            }

            StringTokenizer tokenizer = new StringTokenizer(line);
            while (tokenizer.hasMoreTokens())
            {
              buffer.append(tokenizer.nextToken());
            }
          }

          if (shouldClose)
          {
            reader.close();
          }

          dataToDecode = buffer.toString();
        }
        catch (Exception e)
        {
          System.err.println(ERR_BASE64_CANNOT_READ_ENCODED_DATA.get(
                                  getExceptionMessage(e)));
          System.exit(1);
        }
      }

      byte[] decodedData = null;
      try
      {
        decodedData = decode(dataToDecode);
      }
      catch (ParseException pe)
      {
        System.err.println(pe.getMessage());
        System.exit(1);
      }

      try
      {
        if (toRawFile.isPresent())
        {
          FileOutputStream outputStream =
               new FileOutputStream(toRawFile.getValue());
          outputStream.write(decodedData);
          outputStream.close();
        }
        else
        {
          System.out.write(decodedData);
          System.out.println();
          System.out.flush();
        }
      }
      catch (Exception e)
      {
        System.err.println(ERR_BASE64_CANNOT_WRITE_RAW_DATA.get(
                                getExceptionMessage(e)));
        System.exit(1);
      }
    }
    else
    {
      System.err.println(ERR_BASE64_UNKNOWN_SUBCOMMAND.get(
                              subCommand.getName()));
      System.exit(1);
    }
  }
}
