text: %% -*- mode: erlang; tab-width: 4; indent-tabs-mode: 1; st-rulers: [70] -*-
  %% vim: ts=4 sw=4 ft=erlang noet
  %%%-------------------------------------------------------------------
  %%% @author Andrew Bennett <potatosaladx@gmail.com>
  %%% @copyright 2017-2019, Andrew Bennett
  %%% @doc RFC 4648, Section 4: https://tools.ietf.org/html/rfc4648#section-4
  %%%
  %%% @end
  %%% Created :  11 May 2017 by Andrew Bennett <potatosaladx@gmail.com>
  %%%-------------------------------------------------------------------
  -module(jose_base64).
  -compile({parse_transform, jose_base}).
  
  -include("jose_base.hrl").
  
  %% API
  -export([decode/1]).
  -export([decode/2]).
  -export(['decode!'/1]).
  -export(['decode!'/2]).
  -export([encode/1]).
  -export([encode/2]).
  -export([random/1]).
  -export([random/2]).
  
  % Macros
  -define(B64_TO_INT(C),
  	case C of
  		$A -> 16#00;
  		$B -> 16#01;
  		$C -> 16#02;
  		$D -> 16#03;
  		$E -> 16#04;
  		$F -> 16#05;
  		$G -> 16#06;
  		$H -> 16#07;
  		$I -> 16#08;
  		$J -> 16#09;
  		$K -> 16#0A;
  		$L -> 16#0B;
  		$M -> 16#0C;
  		$N -> 16#0D;
  		$O -> 16#0E;
  		$P -> 16#0F;
  		$Q -> 16#10;
  		$R -> 16#11;
  		$S -> 16#12;
  		$T -> 16#13;
  		$U -> 16#14;
  		$V -> 16#15;
  		$W -> 16#16;
  		$X -> 16#17;
  		$Y -> 16#18;
  		$Z -> 16#19;
  		$a -> 16#1A;
  		$b -> 16#1B;
  		$c -> 16#1C;
  		$d -> 16#1D;
  		$e -> 16#1E;
  		$f -> 16#1F;
  		$g -> 16#20;
  		$h -> 16#21;
  		$i -> 16#22;
  		$j -> 16#23;
  		$k -> 16#24;
  		$l -> 16#25;
  		$m -> 16#26;
  		$n -> 16#27;
  		$o -> 16#28;
  		$p -> 16#29;
  		$q -> 16#2A;
  		$r -> 16#2B;
  		$s -> 16#2C;
  		$t -> 16#2D;
  		$u -> 16#2E;
  		$v -> 16#2F;
  		$w -> 16#30;
  		$x -> 16#31;
  		$y -> 16#32;
  		$z -> 16#33;
  		$0 -> 16#34;
  		$1 -> 16#35;
  		$2 -> 16#36;
  		$3 -> 16#37;
  		$4 -> 16#38;
  		$5 -> 16#39;
  		$6 -> 16#3A;
  		$7 -> 16#3B;
  		$8 -> 16#3C;
  		$9 -> 16#3D;
  		$+ -> 16#3E;
  		$/ -> 16#3F
  	end).
  
  -define(INT_TO_B64(C),
  	case C of
  		16#00 -> $A;
  		16#01 -> $B;
  		16#02 -> $C;
  		16#03 -> $D;
  		16#04 -> $E;
  		16#05 -> $F;
  		16#06 -> $G;
  		16#07 -> $H;
  		16#08 -> $I;
  		16#09 -> $J;
  		16#0A -> $K;
  		16#0B -> $L;
  		16#0C -> $M;
  		16#0D -> $N;
  		16#0E -> $O;
  		16#0F -> $P;
  		16#10 -> $Q;
  		16#11 -> $R;
  		16#12 -> $S;
  		16#13 -> $T;
  		16#14 -> $U;
  		16#15 -> $V;
  		16#16 -> $W;
  		16#17 -> $X;
  		16#18 -> $Y;
  		16#19 -> $Z;
  		16#1A -> $a;
  		16#1B -> $b;
  		16#1C -> $c;
  		16#1D -> $d;
  		16#1E -> $e;
  		16#1F -> $f;
  		16#20 -> $g;
  		16#21 -> $h;
  		16#22 -> $i;
  		16#23 -> $j;
  		16#24 -> $k;
  		16#25 -> $l;
  		16#26 -> $m;
  		16#27 -> $n;
  		16#28 -> $o;
  		16#29 -> $p;
  		16#2A -> $q;
  		16#2B -> $r;
  		16#2C -> $s;
  		16#2D -> $t;
  		16#2E -> $u;
  		16#2F -> $v;
  		16#30 -> $w;
  		16#31 -> $x;
  		16#32 -> $y;
  		16#33 -> $z;
  		16#34 -> $0;
  		16#35 -> $1;
  		16#36 -> $2;
  		16#37 -> $3;
  		16#38 -> $4;
  		16#39 -> $5;
  		16#3A -> $6;
  		16#3B -> $7;
  		16#3C -> $8;
  		16#3D -> $9;
  		16#3E -> $+;
  		16#3F -> $/
  	end).
  
  %%%===================================================================
  %%% API functions
  %%%===================================================================
  
  decode(Input) when ?is_iodata(Input) ->
  	decode(Input, #{}).
  
  decode(Input, Opts) when ?is_iodata(Input) andalso is_map(Opts) ->
  	try 'decode!'(Input, Opts) of
  		Output when is_binary(Output) ->
  			{ok, Output}
  	catch
  		_:_ ->
  			error
  	end;
  decode(Input, Opts) when ?is_iodata(Input) andalso is_list(Opts) ->
  	decode(Input, maps:from_list(Opts)).
  
  'decode!'(Input) when ?is_iodata(Input) ->
  	'decode!'(Input, #{}).
  
  'decode!'([], #{}) ->
  	<<>>;
  'decode!'(<<>>, #{}) ->
  	<<>>;
  'decode!'(Input, Opts) when ?is_iodata(Input) andalso is_map(Opts) ->
  	Padding = maps:get('padding', Opts, nil),
  	Size = erlang:iolist_size(Input),
  	Offset =
  		case Padding of
  			_ when (Padding == false orelse Padding == nil) andalso Size =< 4 ->
  				0;
  			_ when (Padding == false orelse Padding == nil) andalso (Size rem 4) =/= 0 ->
  				Size - (Size rem 4);
  			_ when (Padding == false orelse Padding == nil) ->
  				Size - 4;
  			_ when (Padding == true orelse Padding == nil) andalso Size >= 4 ->
  				Size - 4;
  			_ ->
  				erlang:error({badarg, [Input, Opts]})
  		end,
  	<< Head0:Offset/binary, Tail0/binary >> = ?to_binary(Input),
  	Head = << << (?B64_TO_INT(V)):6 >> || << V >> <= Head0 >>,
  	Tail =
  		case Padding of
  			false ->
  				case Tail0 of
  					<< T0:8, T1:8 >> ->
  						<< (?B64_TO_INT(T0)):6, (?B64_TO_INT(T1) bsr 4):2 >>;
  					<< T0:8, T1:8, T2:8 >> ->
  						<< (?B64_TO_INT(T0)):6, (?B64_TO_INT(T1)):6, (?B64_TO_INT(T2) bsr 2):4 >>;
  					<< T:4/binary >> ->
  						<< << (?B64_TO_INT(V)):6 >> || << V >> <= T >>;
  					<<>> ->
  						<<>>;
  					_ ->
  						erlang:error({badarg, [Input, Opts]})
  				end;
  			nil ->
  				case Tail0 of
  					<< T0:8, T1:8, $=, $= >> ->
  						<< (?B64_TO_INT(T0)):6, (?B64_TO_INT(T1) bsr 4):2 >>;
  					<< T0:8, T1:8, T2:8, $= >> ->
  						<< (?B64_TO_INT(T0)):6, (?B64_TO_INT(T1)):6, (?B64_TO_INT(T2) bsr 2):4 >>;
  					<< T0:8, T1:8 >> ->
  						<< (?B64_TO_INT(T0)):6, (?B64_TO_INT(T1) bsr 4):2 >>;
  					<< T0:8, T1:8, T2:8 >> ->
  						<< (?B64_TO_INT(T0)):6, (?B64_TO_INT(T1)):6, (?B64_TO_INT(T2) bsr 2):4 >>;
  					<< T:4/binary >> ->
  						<< << (?B64_TO_INT(V)):6 >> || << V >> <= T >>;
  					<<>> ->
  						<<>>
  				end;
  			true ->
  				case Tail0 of
  					<< T0:8, T1:8, $=, $= >> ->
  						<< (?B64_TO_INT(T0)):6, (?B64_TO_INT(T1) bsr 4):2 >>;
  					<< T0:8, T1:8, T2:8, $= >> ->
  						<< (?B64_TO_INT(T0)):6, (?B64_TO_INT(T1)):6, (?B64_TO_INT(T2) bsr 2):4 >>;
  					<< T:4/binary >> ->
  						<< << (?B64_TO_INT(V)):6 >> || << V >> <= T >>;
  					<<>> ->
  						<<>>;
  					_ ->
  						erlang:error({badarg, [Input, Opts]})
  				end
  		end,
  	<< Head/binary, Tail/binary >>;
  'decode!'(Input, Opts) when ?is_iodata(Input) andalso is_list(Opts) ->
  	'decode!'(Input, maps:from_list(Opts)).
  
  encode(Input) when ?is_iodata(Input) ->
  	encode(Input, #{}).
  
  encode(Input, Opts) when ?is_iodata(Input) andalso is_map(Opts) ->
  	Padding = maps:get('padding', Opts, true),
  	Offset = 6 * (erlang:iolist_size(Input) div 6),
  	<< Head:Offset/binary, Tail/binary >> = ?to_binary(Input),
  	H = << << (encode_pair(V0)):16, (encode_pair(V1)):16, (encode_pair(V2)):16, (encode_pair(V3)):16 >> || << V0:12, V1:12, V2:12, V3:12 >> <= Head >>,
  	{T, Pad} =
  		case Tail of
  			<< T0:12, T1:12, T2:12, T3:4 >> ->
  				{<< (encode_pair(T0)):16, (encode_pair(T1)):16, (encode_pair(T2)):16, (encode_char(T3 bsl 2)):8 >>, << $= >>};
  			<< T0:12, T1:12, T2:8 >> ->
  				{<< (encode_pair(T0)):16, (encode_pair(T1)):16, (encode_pair(T2 bsl 4)):16 >>, << $=, $= >>};
  			<< T0:12, T1:12 >> ->
  				{<< (encode_pair(T0)):16, (encode_pair(T1)):16 >>, <<>>};
  			<< T0:12, T1:4 >> ->
  				{<< (encode_pair(T0)):16, (encode_char(T1 bsl 2)):8 >>, <<>>};
  			<< T0:8 >> ->
  				{<< (encode_pair(T0 bsl 4)):16 >>, << $=, $= >>};
  			<<>> ->
  				{<<>>, <<>>}
  		end,
  	case Padding of
  		true ->
  			<< H/binary, T/binary, Pad/binary >>;
  		false ->
  			<< H/binary, T/binary >>;
  		_ ->
  			erlang:error({badarg, [Input, Opts]})
  	end;
  encode(Input, Opts) when ?is_iodata(Input) andalso is_list(Opts) ->
  	encode(Input, maps:from_list(Opts)).
  
  random(Bytes) when is_integer(Bytes) andalso Bytes >= 0 ->
  	random(Bytes, #{}).
  
  random(0, Opts) when is_map(Opts) ->
  	<<>>;
  random(Bytes, Opts) when (Bytes =:= 1) andalso is_map(Opts) ->
  	erlang:error({badarg, [Bytes, Opts]});
  random(Bytes, Opts) when is_integer(Bytes) andalso Bytes > 0 andalso is_map(Opts) ->
  	Padding = maps:get('padding', Opts, true),
  	R = (Bytes rem 4),
  	Size =
  		case Padding of
  			true when R =:= 0 ->
  				(Bytes * 3) div 4;
  			false when R =:= 0 orelse R =:= 2 orelse R =:= 3 ->
  				(Bytes * 3) div 4;
  			_ ->
  				erlang:error({badarg, [Bytes, Opts]})
  		end,
  	Binary = crypto:strong_rand_bytes(Size),
  	encode(Binary, Opts);
  random(Bytes, Opts) when is_integer(Bytes) andalso Bytes >= 0 andalso is_list(Opts) ->
  	random(Bytes, maps:from_list(Opts)).
  
  %%%-------------------------------------------------------------------
  %%% Internal functions
  %%%-------------------------------------------------------------------
  
  %% @private
  encode_char(V) ->
  	jose_base:encode_char(?INT_TO_B64(V)).
  
  %% @private
  encode_pair(V) ->
  	jose_base:encode_pair(?INT_TO_B64(V), sensitive).
  