base64_encode :: (str: string, $for_url := false) -> string, bool {
   enc, ok := base64_encode(str.([]u8), for_url);
   return enc.(string), ok;
}

base64_decode :: (str: string) -> string, bool {
   enc, ok := base64_decode(str.([]u8));
   return enc.(string), ok;
}

base64_encode :: ($$data: []u8, $for_url := false) -> []u8, bool {
   if data.count == 0 return .[], false;

   #if for_url {
      Character_Set :: Encoding_Url;
   }
   else {
      Character_Set :: Encoding_Standard;
   }

   padded  := strings.contains(data.(string), Padding_Character);
   encoded := basic.NewArray(encoded_length(data.count, padded), u8, true);

   src_idx := 0;
   dst_idx := 0;
   src_max := (data.count / 3) * 3;
   while src_idx < src_max {
      value: u64;

      parts := (*value).(*u8);
      (parts + 0).* = xx data[src_idx + 2];
      (parts + 1).* = xx data[src_idx + 1];
      (parts + 2).* = xx data[src_idx + 0];

      encoded[dst_idx + 0] = Character_Set[value >> 18 & 0x3F];
      encoded[dst_idx + 1] = Character_Set[value >> 12 & 0x3F];
      encoded[dst_idx + 2] = Character_Set[value >>  6 & 0x3F];
      encoded[dst_idx + 3] = Character_Set[value       & 0x3F];

      src_idx += 3;
      dst_idx += 4;
   }

   remaining := data.count - src_idx;
   if !remaining then return encoded, true;

   value := data[src_idx + 0].(u64) << 16;
   if remaining == 2 {
      value |= data[src_idx + 1].(u64) << 8;
   }

   encoded[dst_idx + 0] = Character_Set[value >> 18 & 0x3F];
   encoded[dst_idx + 1] = Character_Set[value >> 12 & 0x3F];

   if remaining == {
      case 2;
         if padded {
            encoded[dst_idx + 2] = Padding_Character;
            encoded[dst_idx + 3] = Padding_Character;
         }
      case 1;
         encoded[dst_idx + 2] = Character_Set[value >> 6 & 0x3F];
         if padded {
            encoded[dst_idx + 3] = Padding_Character;
         }
   }

   return encoded, true;
}

base64_decode :: (data: []u8) -> []u8, bool {
   if !data.count return .[], false;

   lookup :: (c: u8) -> u8 #expand {
      if c >= #char "A" && c <= #char "Z" return c - #char "A";
      if c >= #char "a" && c <= #char "z" return c - 71;
      if c >= #char "0" && c <= #char "9" return c + 4;
      if c == #char "+" || c == #char "-" return 62;
      if c == #char "/" || c == #char "_" return 63;
      return 255;
   }

   a4_to_a3 :: () #expand {
      `a3[0] = (`a4[0] << 2) + ((`a4[1] & 0x30) >> 4);
      `a3[1] = ((`a4[1] & 0xF) << 4) + ((`a4[2] & 0x3C) >> 2);
      `a3[2] = ((`a4[2] & 0x3) << 6) + `a4[3];
   }

   padding := 0;
   {
      i := data.count - 1;
      while i >= 0 {
         if data[i] == Padding_Character {
            padding += 1;
         }
         else {
            break;
         }

         i -= 1;
      }
   }

   a3: [3]u8;
   a4: [4]u8;

   i      := 0;
   offset := 0;

   decoded := basic.NewArray(((6 * data.count) / 8) - padding, u8, true);
   for 0..data.count - 1 {
      chr := data[it];
      if chr == Padding_Character break;

      a4[i] = chr;
      i += 1;

      if i == 4 {
         for 0..3 a4[it] = lookup(a4[it]);

         a4_to_a3();

         for 0..2 {
            decoded.data[offset] = a3[it];
            offset += 1;
         }

         i = 0;
      }
   }

   if i > 0 {
      for 0..3 a4[it] = lookup(a4[it]);

      a4_to_a3();

      for 0..i - 1 {
         (decoded.data + offset + it).* = a3[it];
      }
   }

   return decoded, true;
}

#scope_file;

Padding_Character :: #char "=";
Encoding_Url      :: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
Encoding_Standard :: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

encoded_length :: (count: int, with_padding := false) -> int {
   if with_padding then return (count + 2) / 3 * 4;
   return (count * 8 + 5) / 6;
}


#scope_file;

basic   :: #import "Basic";
strings :: #import "String";


#if RUN_TESTS #run {
   test.run("encodes", (t) => {
      str :: "Hello, World";

      encoded, ok := base64_encode(str);
      test.expect(t, ok, "'%' did not properly encode!", str);

      expected :: "SGVsbG8sIFdvcmxk";
      test.expect(t, encoded == expected, "wanted: '%', received: '%'", expected, encoded);
   });

   test.run("decodes", (t) => {
      encoded_str :: "SGVsbG8sIFdvcmxk";

      decoded, ok := base64_decode(encoded_str);
      test.expect(t, ok, "'%' did not properly decode!", encoded_str);

      expected :: "Hello, World";
      test.expect(t, decoded == expected, "wanted: '%', received: '%'", expected, decoded);
   });

   test.run("encodes/decodes padding", (t) => {
      str :: "VkdocGN5QnBjeUJ6YjIxbGRHaHBibWNnYlc5eVpTQmpiMjF3YkdWNExDQnBkQ0J6YUc5MWJHUWdhR0YyWlNCd1lXUmthVzVuUHc9PQ==";

      first_decode, f_ok := base64_decode(str);
      test.expect(t, f_ok, "first decode failed!");

      re_encode, r_ok := base64_encode(first_decode);
      test.expect(t, r_ok, "re-encode failed!");

      second_decode, s_ok := base64_decode(re_encode);
      test.expect(t, s_ok, "second decode failed!");

      for 0..first_decode.count - 1 {
         test.expect(t, first_decode[it] == second_decode[it], "Decoded byte '%' did not match expected output", it);
      }
   });

   test.run("encodes/decodes struct", (t) => {
      Foo :: struct {
         first:  u64;
         second: u64;
         third:  u64;
         forth:  u64;
      }

      foo: Foo;
      foo.first  = 123;
      foo.second = 456;
      foo.third  = 789;
      foo.forth  = 100;

      enc_builder: basic.String_Builder;
      basic.print_to_builder(*enc_builder, "%,%,%,%", foo.first, foo.second, foo.third, foo.forth);

      encoded, e_ok := base64_encode(basic.builder_to_string(*enc_builder));
      test.expect(t, e_ok, "Encode of structure failed!");

      decoded, d_ok := base64_decode(encoded);
      test.expect(t, d_ok, "Decode of encoded structure failed!");

      parts := strings.split(decoded, ",");
      test.expect(t, parts.count == 4, "Invalid number of parts after decode: %", parts.count);

      bar: Foo = ---;

      {
         ptr  := (*bar).(*u8);
         info := Foo.(*Type_Info_Struct);

         for info.members {
            value, ok := strings.parse_int(*parts[it_index], u64);
            test.expect(t, ok, "Integer parse of value % ('%') failed!", it_index, parts[it_index]);

            offset := (ptr + it.offset_in_bytes).(*u64);
            offset.*= value;
         }
      }

      test.expect(t, foo.first  == bar.first,  "Foo.first (%, %) didn't match between encoding/decoding!", foo.first, bar.first);
      test.expect(t, foo.second == bar.second, "Foo.second (%, %) didn't match between encoding/decoding!", foo.second, bar.second);
      test.expect(t, foo.third  == bar.third,  "Foo.third (%, %) didn't match between encoding/decoding!", foo.third, bar.third);
      test.expect(t, foo.forth  == bar.forth,  "Foo.forth (%, %) didn't match between encoding/decoding!", foo.forth, bar.forth);
   });
}


