/**
 * Base64 encoder/decoder for arduino repo
 * Uses common web conventions - '+' for 62, '/' for 63, '=' for padding.
 * Note that invalid base64 characters are interpreted as padding.
 * Author: Densaugeo
 * Maintainer: Densaugeo
 * Version: 1.2.1.1
 * Changed unsigned int to uint16_t for use in the professional Ender-3V2/S1 firmware
 * Url: https://www.arduino.cc/reference/en/libraries/base64/
 */
#pragma once

/* binary_to_base64:
 *   Description:
 *     Converts a single byte from a binary value to the corresponding base64 character
 *   Parameters:
 *     v - Byte to convert
 *   Returns:
 *     ascii code of base64 character. If byte is >= 64, then there is not corresponding base64 character
 *     and 255 is returned
 */
unsigned char binary_to_base64(unsigned char v);

/* base64_to_binary:
 *   Description:
 *     Converts a single byte from a base64 character to the corresponding binary value
 *   Parameters:
 *     c - Base64 character (as ascii code)
 *   Returns:
 *     6-bit binary value
 */
unsigned char base64_to_binary(unsigned char c);

/* encode_base64_length:
 *   Description:
 *     Calculates length of base64 string needed for a given number of binary bytes
 *   Parameters:
 *     input_length - Amount of binary data in bytes
 *   Returns:
 *     Number of base64 characters needed to encode input_length bytes of binary data
 */
uint16_t encode_base64_length(uint16_t input_length);

/* decode_base64_length:
 *   Description:
 *     Calculates number of bytes of binary data in a base64 string
 *     Variant that does not use input_length no longer used within library, retained for API compatibility
 *   Parameters:
 *     input - Base64-encoded null-terminated string
 *     input_length (optional) - Number of bytes to read from input pointer
 *   Returns:
 *     Number of bytes of binary data in input
 */
uint16_t decode_base64_length(unsigned char input[]);
uint16_t decode_base64_length(unsigned char input[], uint16_t input_length);

/* encode_base64:
 *   Description:
 *     Converts an array of bytes to a base64 null-terminated string
 *   Parameters:
 *     input - Pointer to input data
 *     input_length - Number of bytes to read from input pointer
 *     output - Pointer to output string. Null terminator will be added automatically
 *   Returns:
 *     Length of encoded string in bytes (not including null terminator)
 */
uint16_t encode_base64(unsigned char input[], uint16_t input_length, unsigned char output[]);

/* decode_base64:
 *   Description:
 *     Converts a base64 null-terminated string to an array of bytes
 *   Parameters:
 *     input - Pointer to input string
 *     input_length (optional) - Number of bytes to read from input pointer
 *     output - Pointer to output array
 *   Returns:
 *     Number of bytes in the decoded binary
 */
uint16_t decode_base64(unsigned char input[], unsigned char output[]);
uint16_t decode_base64(unsigned char input[], uint16_t input_length, unsigned char output[]);

unsigned char binary_to_base64(unsigned char v) {
  // Capital letters - 'A' is ascii 65 and base64 0
  if (v < 26) return v + 'A';

  // Lowercase letters - 'a' is ascii 97 and base64 26
  if (v < 52) return v + 71;

  // Digits - '0' is ascii 48 and base64 52
  if (v < 62) return v - 4;

  // '+' is ascii 43 and base64 62
  if (v == 62) return '+';

  // '/' is ascii 47 and base64 63
  if (v == 63) return '/';

  return 64;
}

unsigned char base64_to_binary(unsigned char c) {
  // Capital letters - 'A' is ascii 65 and base64 0
  if ('A' <= c && c <= 'Z') return c - 'A';

  // Lowercase letters - 'a' is ascii 97 and base64 26
  if ('a' <= c && c <= 'z') return c - 71;

  // Digits - '0' is ascii 48 and base64 52
  if ('0' <= c && c <= '9') return c + 4;

  // '+' is ascii 43 and base64 62
  if (c == '+') return 62;

  // '/' is ascii 47 and base64 63
  if (c == '/') return 63;

  return 255;
}

uint16_t encode_base64_length(uint16_t input_length) {
  return (input_length + 2)/3*4;
}

uint16_t decode_base64_length(unsigned char input[]) {
  return decode_base64_length(input, -1);
}

uint16_t decode_base64_length(unsigned char input[], uint16_t input_length) {
  unsigned char *start = input;

  while (base64_to_binary(input[0]) < 64 && (unsigned char)(input - start) < input_length) {
    ++input;
  }

  input_length = input - start;
  return input_length / 4 * 3 + (input_length % 4 ? input_length % 4 - 1 : 0);
}

uint16_t encode_base64(unsigned char input[], uint16_t input_length, unsigned char output[]) {
  uint16_t full_sets = input_length / 3;

  // While there are still full sets of 24 bits...
  for (uint16_t i = 0; i < full_sets; ++i) {
    output[0] = binary_to_base64(                         input[0] >> 2);
    output[1] = binary_to_base64((input[0] & 0x03) << 4 | input[1] >> 4);
    output[2] = binary_to_base64((input[1] & 0x0F) << 2 | input[2] >> 6);
    output[3] = binary_to_base64( input[2] & 0x3F);

    input += 3;
    output += 4;
  }

  switch (input_length % 3) {
    case 0:
      output[0] = '\0';
      break;
    case 1:
      output[0] = binary_to_base64(                         input[0] >> 2);
      output[1] = binary_to_base64((input[0] & 0x03) << 4);
      output[2] = '=';
      output[3] = '=';
      output[4] = '\0';
      break;
    case 2:
      output[0] = binary_to_base64(                         input[0] >> 2);
      output[1] = binary_to_base64((input[0] & 0x03) << 4 | input[1] >> 4);
      output[2] = binary_to_base64((input[1] & 0x0F) << 2);
      output[3] = '=';
      output[4] = '\0';
      break;
  }

  return encode_base64_length(input_length);
}

uint16_t decode_base64(unsigned char input[], unsigned char output[]) {
  return decode_base64(input, -1, output);
}

uint16_t decode_base64(unsigned char input[], uint16_t input_length, unsigned char output[]) {
  uint16_t output_length = decode_base64_length(input, input_length);

  // While there are still full sets of 24 bits...
  for (uint16_t i = 2; i < output_length; i += 3) {
    output[0] = base64_to_binary(input[0]) << 2 | base64_to_binary(input[1]) >> 4;
    output[1] = base64_to_binary(input[1]) << 4 | base64_to_binary(input[2]) >> 2;
    output[2] = base64_to_binary(input[2]) << 6 | base64_to_binary(input[3]);

    input += 4;
    output += 3;
  }

  switch (output_length % 3) {
    case 1:
      output[0] = base64_to_binary(input[0]) << 2 | base64_to_binary(input[1]) >> 4;
      break;
    case 2:
      output[0] = base64_to_binary(input[0]) << 2 | base64_to_binary(input[1]) >> 4;
      output[1] = base64_to_binary(input[1]) << 4 | base64_to_binary(input[2]) >> 2;
      break;
  }

  return output_length;
}
