/*
* Base Encoding and Decoding
* (C) 2018 Erwan Chaussy
* (C) 2018 Jack Lloyd
*
* Botan is released under the Simplified BSD License (see license.txt)
*/

#ifndef BOTAN_BASE_CODEC_H_
#define BOTAN_BASE_CODEC_H_

#include <botan/exceptn.h>
#include <botan/mem_ops.h>
#include <array>
#include <string>
#include <type_traits>

namespace Botan {

/**
* Perform encoding using the base provided
* @param base object giving access to the encodings specifications
* @param output an array of at least base.encode_max_output bytes
* @param input is some binary data
* @param input_length length of input in bytes
* @param input_consumed is an output parameter which says how many
*        bytes of input were actually consumed. If less than
*        input_length, then the range input[consumed:length]
*        should be passed in later along with more input.
* @param final_inputs true iff this is the last input, in which case
         padding chars will be applied if needed
* @return number of bytes written to output
*/
template <class Base>
size_t base_encode(const Base& base,
                   char output[],
                   const uint8_t input[],
                   size_t input_length,
                   size_t& input_consumed,
                   bool final_inputs) {
   input_consumed = 0;

   // TODO(Botan4) Check if we can use just base. or Base:: here instead
   constexpr size_t encoding_bytes_in = std::remove_reference_t<Base>::encoding_bytes_in();
   constexpr size_t encoding_bytes_out = std::remove_reference_t<Base>::encoding_bytes_out();

   size_t input_remaining = input_length;
   size_t output_produced = 0;

   while(input_remaining >= encoding_bytes_in) {
      base.encode(output + output_produced, input + input_consumed);

      input_consumed += encoding_bytes_in;
      output_produced += encoding_bytes_out;
      input_remaining -= encoding_bytes_in;
   }

   if(final_inputs && input_remaining) {
      std::array<uint8_t, encoding_bytes_in> remainder{};
      for(size_t i = 0; i != input_remaining; ++i) {
         remainder[i] = input[input_consumed + i];
      }

      base.encode(output + output_produced, remainder.data());

      const size_t bits_consumed = base.bits_consumed();
      const size_t remaining_bits_before_padding = base.remaining_bits_before_padding();

      size_t empty_bits = 8 * (encoding_bytes_in - input_remaining);
      size_t index = output_produced + encoding_bytes_out - 1;
      while(empty_bits >= remaining_bits_before_padding) {
         output[index--] = '=';
         empty_bits -= bits_consumed;
      }

      input_consumed += input_remaining;
      output_produced += encoding_bytes_out;
   }

   return output_produced;
}

template <typename Base>
std::string base_encode_to_string(const Base& base, const uint8_t input[], size_t input_length) {
   const size_t output_length = base.encode_max_output(input_length);
   std::string output(output_length, 0);

   size_t consumed = 0;
   size_t produced = 0;

   if(output_length > 0) {
      produced = base_encode(base, &output.front(), input, input_length, consumed, true);
   }

   BOTAN_ASSERT_EQUAL(consumed, input_length, "Consumed the entire input");
   BOTAN_ASSERT_EQUAL(produced, output.size(), "Produced expected size");

   return output;
}

/**
* Perform decoding using the base provided
* @param base object giving access to the encodings specifications
* @param output an array of at least Base::decode_max_output bytes
* @param input some base input
* @param input_length length of input in bytes
* @param input_consumed is an output parameter which says how many
*        bytes of input were actually consumed. If less than
*        input_length, then the range input[consumed:length]
*        should be passed in later along with more input.
* @param final_inputs true iff this is the last input, in which case
         padding is allowed
* @param ignore_ws ignore whitespace on input; if false, throw an
                   exception if whitespace is encountered
* @return number of bytes written to output
*/
template <typename Base>
size_t base_decode(const Base& base,
                   uint8_t output[],
                   const char input[],
                   size_t input_length,
                   size_t& input_consumed,
                   bool final_inputs,
                   bool ignore_ws = true) {
   // TODO(Botan4) Check if we can use just base. or Base:: here instead
   constexpr size_t decoding_bytes_in = std::remove_reference_t<Base>::decoding_bytes_in();
   constexpr size_t decoding_bytes_out = std::remove_reference_t<Base>::decoding_bytes_out();

   input_consumed = 0;

   uint8_t* out_ptr = output;
   std::array<uint8_t, decoding_bytes_in> decode_buf{};
   size_t decode_buf_pos = 0;
   size_t final_truncate = 0;
   bool seen_padding = false;

   clear_mem(output, base.decode_max_output(input_length));

   for(size_t i = 0; i != input_length; ++i) {
      const uint8_t bin = base.lookup_binary_value(input[i]);

      // This call might throw Invalid_Argument
      if(base.check_bad_char(bin, input[i], ignore_ws)) {
         // Padding may only appear at the end, so a data symbol must never
         // follow one (0x81 marks a padding character)
         if(seen_padding) {
            throw Invalid_Argument(base.name() + " decoding failed, data follows padding");
         }
         decode_buf[decode_buf_pos] = bin;
         ++decode_buf_pos;
      } else if(bin == 0x81) {
         seen_padding = true;
      }

      /*
      * If we're at the end of the input, pad with 0s and truncate
      */
      if(final_inputs && (i == input_length - 1)) {
         if(decode_buf_pos) {
            const size_t bits_per_symbol = base.bits_consumed();
            const size_t pad_bits = (decode_buf_pos * bits_per_symbol) % 8;

            // A trailing symbol contributing only pad bits cannot occur in a
            // valid encoding; RFC 4648 4 and 6 enumerate the reachable cases
            if(pad_bits >= bits_per_symbol) {
               throw Invalid_Argument(base.name() + " decoding failed, invalid length");
            }

            // RFC 4648 3.5: "decoders MAY chose to reject an encoding if the
            // pad bits have not been set to zero"
            const uint8_t pad_mask = static_cast<uint8_t>((1U << pad_bits) - 1);
            if(decode_buf[decode_buf_pos - 1] & pad_mask) {
               throw Invalid_Argument(base.name() + " decoding failed, nonzero padding bits");
            }

            for(size_t j = decode_buf_pos; j < decoding_bytes_in; ++j) {
               decode_buf[j] = 0;
            }

            final_truncate = decoding_bytes_in - decode_buf_pos;
            decode_buf_pos = decoding_bytes_in;
         }
      }

      if(decode_buf_pos == decoding_bytes_in) {
         base.decode(out_ptr, decode_buf.data());

         out_ptr += decoding_bytes_out;
         decode_buf_pos = 0;
         input_consumed = i + 1;
      }
   }

   while(input_consumed < input_length && base.lookup_binary_value(input[input_consumed]) == 0x80) {
      ++input_consumed;
   }

   const size_t written = (out_ptr - output) - base.bytes_to_remove(final_truncate);

   return written;
}

template <typename Base>
size_t base_decode_full(const Base& base, uint8_t output[], const char input[], size_t input_length, bool ignore_ws) {
   size_t consumed = 0;
   const size_t written = base_decode(base, output, input, input_length, consumed, true, ignore_ws);

   if(consumed != input_length) {
      throw Invalid_Argument(base.name() + " decoding failed, input did not have full bytes");
   }

   return written;
}

template <typename Vector, typename Base>
Vector base_decode_to_vec(const Base& base, const char input[], size_t input_length, bool ignore_ws) {
   const size_t output_length = base.decode_max_output(input_length);
   Vector bin(output_length);

   const size_t written = base_decode_full(base, bin.data(), input, input_length, ignore_ws);

   bin.resize(written);
   return bin;
}

}  // namespace Botan

#endif
