defmodule Base do
  import Bitwise

  @moduledoc """
  This module provides data encoding and decoding functions
  according to [RFC 4648](http://tools.ietf.org/html/rfc4648).

  This document defines the commonly used base 16, base 32, and base
  64 encoding schemes.

  ## Base 16 alphabet

      | Value | Encoding | Value | Encoding | Value | Encoding | Value | Encoding |
      |------:|---------:|------:|---------:|------:|---------:|------:|---------:|
      |      0|         0|      4|         4|      8|         8|     12|         C|
      |      1|         1|      5|         5|      9|         9|     13|         D|
      |      2|         2|      6|         6|     10|         A|     14|         E|
      |      3|         3|      7|         7|     11|         B|     15|         F|

  ## Base 32 alphabet

      | Value | Encoding | Value | Encoding | Value | Encoding | Value | Encoding |
      |------:|---------:|------:|---------:|------:|---------:|------:|---------:|
      |      0|         A|      9|         J|     18|         S|     27|         3|
      |      1|         B|     10|         K|     19|         T|     28|         4|
      |      2|         C|     11|         L|     20|         U|     29|         5|
      |      3|         D|     12|         M|     21|         V|     30|         6|
      |      4|         E|     13|         N|     22|         W|     31|         7|
      |      5|         F|     14|         O|     23|         X|       |          |
      |      6|         G|     15|         P|     24|         Y|  (pad)|         =|
      |      7|         H|     16|         Q|     25|         Z|       |          |
      |      8|         I|     17|         R|     26|         2|       |          |


  ## Base 32 (extended hex) alphabet

      | Value | Encoding | Value | Encoding | Value | Encoding | Value | Encoding |
      |------:|---------:|------:|---------:|------:|---------:|------:|---------:|
      |      0|         0|      9|         9|     18|         I|     27|         R|
      |      1|         1|     10|         A|     19|         J|     28|         S|
      |      2|         2|     11|         B|     20|         K|     29|         T|
      |      3|         3|     12|         C|     21|         L|     30|         U|
      |      4|         4|     13|         D|     22|         M|     31|         V|
      |      5|         5|     14|         E|     23|         N|       |          |
      |      6|         6|     15|         F|     24|         O|  (pad)|         =|
      |      7|         7|     16|         G|     25|         P|       |          |
      |      8|         8|     17|         H|     26|         Q|       |          |

  ## Base 64 alphabet

      | Value | Encoding | Value | Encoding | Value | Encoding | Value | Encoding |
      |------:|---------:|------:|---------:|------:|---------:|------:|---------:|
      |      0|         A|     17|         R|     34|         i|     51|         z|
      |      1|         B|     18|         S|     35|         j|     52|         0|
      |      2|         C|     19|         T|     36|         k|     53|         1|
      |      3|         D|     20|         U|     37|         l|     54|         2|
      |      4|         E|     21|         V|     38|         m|     55|         3|
      |      5|         F|     22|         W|     39|         n|     56|         4|
      |      6|         G|     23|         X|     40|         o|     57|         5|
      |      7|         H|     24|         Y|     41|         p|     58|         6|
      |      8|         I|     25|         Z|     42|         q|     59|         7|
      |      9|         J|     26|         a|     43|         r|     60|         8|
      |     10|         K|     27|         b|     44|         s|     61|         9|
      |     11|         L|     28|         c|     45|         t|     62|         +|
      |     12|         M|     29|         d|     46|         u|     63|         /|
      |     13|         N|     30|         e|     47|         v|       |          |
      |     14|         O|     31|         f|     48|         w|  (pad)|         =|
      |     15|         P|     32|         g|     49|         x|       |          |
      |     16|         Q|     33|         h|     50|         y|       |          |

  ## Base 64 (URL and filename safe) alphabet

      | Value | Encoding | Value | Encoding | Value | Encoding | Value | Encoding |
      |------:|---------:|------:|---------:|------:|---------:|------:|---------:|
      |      0|         A|     17|         R|     34|         i|     51|         z|
      |      1|         B|     18|         S|     35|         j|     52|         0|
      |      2|         C|     19|         T|     36|         k|     53|         1|
      |      3|         D|     20|         U|     37|         l|     54|         2|
      |      4|         E|     21|         V|     38|         m|     55|         3|
      |      5|         F|     22|         W|     39|         n|     56|         4|
      |      6|         G|     23|         X|     40|         o|     57|         5|
      |      7|         H|     24|         Y|     41|         p|     58|         6|
      |      8|         I|     25|         Z|     42|         q|     59|         7|
      |      9|         J|     26|         a|     43|         r|     60|         8|
      |     10|         K|     27|         b|     44|         s|     61|         9|
      |     11|         L|     28|         c|     45|         t|     62|         -|
      |     12|         M|     29|         d|     46|         u|     63|         _|
      |     13|         N|     30|         e|     47|         v|       |          |
      |     14|         O|     31|         f|     48|         w|  (pad)|         =|
      |     15|         P|     32|         g|     49|         x|       |          |
      |     16|         Q|     33|         h|     50|         y|       |          |

  """

  b16_alphabet    = Enum.with_index '0123456789ABCDEF'
  b64_alphabet    = Enum.with_index 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
  b64url_alphabet = Enum.with_index 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'
  b32_alphabet    = Enum.with_index 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'
  b32hex_alphabet = Enum.with_index '0123456789ABCDEFGHIJKLMNOPQRSTUV'

  Enum.each [ {:enc16,    :dec16,    b16_alphabet},
              {:enc64,    :dec64,    b64_alphabet},
              {:enc32,    :dec32,    b32_alphabet},
              {:enc64url, :dec64url, b64url_alphabet},
              {:enc32hex, :dec32hex, b32hex_alphabet} ], fn({enc, dec, alphabet}) ->
    for {encoding, value} <- alphabet do
      defp unquote(enc)(unquote(value)), do: unquote(encoding)
      defp unquote(dec)(unquote(encoding)), do: unquote(value)
    end
    defp unquote(dec)(c) do
      raise ArgumentError, "non-alphabet digit found: #{<<c>>}"
    end
  end

  defp encode_case(:upper, func),
    do: func
  defp encode_case(:lower, func),
    do: &to_lower(func.(&1))

  defp decode_case(:upper, func),
    do: func
  defp decode_case(:lower, func),
    do: &func.(from_lower(&1))
  defp decode_case(:mixed, func),
    do: &func.(from_mixed(&1))

  defp to_lower(char) when char in ?A..?Z,
    do: char + (?a - ?A)
  defp to_lower(char),
    do: char

  defp from_lower(char) when char in ?a..?z,
    do: char - (?a - ?A)
  defp from_lower(char) when not char in ?A..?Z,
    do: char
  defp from_lower(char),
    do: raise(ArgumentError, "non-alphabet digit found: #{<<char>>}")

  defp from_mixed(char) when char in ?a..?z,
    do: char - (?a - ?A)
  defp from_mixed(char),
    do: char

  @doc """
  Encodes a binary string into a base 16 encoded string.

  Accepts an atom `:upper` (default) for encoding to upper case characters or
  `:lower` for lower case characters.

  ## Examples

      iex> Base.encode16("foobar")
      "666F6F626172"

      iex> Base.encode16("foobar", case: :lower)
      "666f6f626172"

  """
  @spec encode16(binary) :: binary
  @spec encode16(binary, Keyword.t) :: binary
  def encode16(data, opts \\ []) when is_binary(data) do
    case = Keyword.get(opts, :case, :upper)
    do_encode16(data, encode_case(case, &enc16/1))
  end


  @doc """
  Decodes a base 16 encoded string into a binary string.

  Accepts an atom `:upper` (default) for decoding from upper case characters or
  `:lower` for lower case characters. `:mixed` can be given for mixed case
  characters.

  ## Examples

      iex> Base.decode16("666F6F626172")
      {:ok, "foobar"}

      iex> Base.decode16("666f6f626172", case: :lower)
      {:ok, "foobar"}

      iex> Base.decode16("666f6F626172", case: :mixed)
      {:ok, "foobar"}

  """
  @spec decode16(binary) :: {:ok, binary} | :error
  @spec decode16(binary, Keyword.t) :: {:ok, binary} | :error
  def decode16(string, opts \\ []) when is_binary(string) do
    case = Keyword.get(opts, :case, :upper)
    {:ok, do_decode16(string, decode_case(case, &dec16/1))}
  rescue
    ArgumentError -> :error
  end

  @doc """
  Decodes a base 16 encoded string into a binary string.

  Accepts an atom `:upper` (default) for decoding from upper case characters or
  `:lower` for lower case characters. `:mixed` can be given for mixed case
  characters.

  An `ArgumentError` exception is raised if the padding is incorrect or
  a non-alphabet character is present in the string.

  ## Examples

      iex> Base.decode16!("666F6F626172")
      "foobar"

      iex> Base.decode16!("666f6f626172", case: :lower)
      "foobar"

      iex> Base.decode16!("666f6F626172", case: :mixed)
      "foobar"

  """
  @spec decode16!(binary) :: binary
  @spec decode16!(binary, Keyword.t) :: binary
  def decode16!(string, opts \\ []) when is_binary(string) do
    case = Keyword.get(opts, :case, :upper)
    do_decode16(string, decode_case(case, &dec16/1))
  end

  @doc """
  Encodes a binary string into a base 64 encoded string.

  ## Examples

      iex> Base.encode64("foobar")
      "Zm9vYmFy"

  """
  @spec encode64(binary) :: binary
  def encode64(data) when is_binary(data) do
    do_encode64(data, &enc64/1)
  end

  @doc """
  Decodes a base 64 encoded string into a binary string.

  ## Examples

      iex> Base.decode64("Zm9vYmFy")
      {:ok, "foobar"}

  """
  @spec decode64(binary) :: {:ok, binary} | :error
  def decode64(string) when is_binary(string) do
    {:ok, do_decode64(string, &dec64/1)}
  rescue
    ArgumentError -> :error
  end

  @doc """
  Decodes a base 64 encoded string into a binary string.

  The following alphabet is used both for encoding and decoding:

  An `ArgumentError` exception is raised if the padding is incorrect or
  a non-alphabet character is present in the string.

  ## Examples

      iex> Base.decode64!("Zm9vYmFy")
      "foobar"

  """
  @spec decode64!(binary) :: binary
  def decode64!(string) when is_binary(string) do
    do_decode64(string, &dec64/1)
  end

  @doc """
  Encodes a binary string into a base 64 encoded string with URL and filename
  safe alphabet.

  ## Examples

      iex> Base.url_encode64(<<255, 127, 254, 252>>)
      "_3_-_A=="

  """
  @spec url_encode64(binary) :: binary
  def url_encode64(data) when is_binary(data) do
    do_encode64(data, &enc64url/1)
  end

  @doc """
  Decodes a base 64 encoded string with URL and filename safe alphabet
  into a binary string.

  ## Examples

      iex> Base.url_decode64("_3_-_A==")
      {:ok, <<255, 127, 254, 252>>}

  """
  @spec url_decode64(binary) :: {:ok, binary} | :error
  def url_decode64(string) when is_binary(string) do
    {:ok, do_decode64(string, &dec64url/1)}
  rescue
    ArgumentError -> :error
  end

  @doc """
  Decodes a base 64 encoded string with URL and filename safe alphabet
  into a binary string.

  An `ArgumentError` exception is raised if the padding is incorrect or
  a non-alphabet character is present in the string.

  ## Examples

      iex> Base.url_decode64!("_3_-_A==")
      <<255, 127, 254, 252>>

  """
  @spec url_decode64!(binary) :: binary
  def url_decode64!(string) when is_binary(string) do
    do_decode64(string, &dec64url/1)
  end

  @doc """
  Encodes a binary string into a base 32 encoded string.

  Accepts an atom `:upper` (default) for encoding to upper case characters or
  `:lower` for lower case characters.

  ## Examples

      iex> Base.encode32("foobar")
      "MZXW6YTBOI======"

      iex> Base.encode32("foobar", case: :lower)
      "mzxw6ytboi======"

  """
  @spec encode32(binary) :: binary
  @spec encode32(binary, Keyword.t) :: binary
  def encode32(data, opts \\ []) when is_binary(data) do
    case = Keyword.get(opts, :case, :upper)
    do_encode32(data, encode_case(case, &enc32/1))
  end

  @doc """
  Decodes a base 32 encoded string into a binary string.

  Accepts an atom `:upper` (default) for decoding from upper case characters or
  `:lower` for lower case characters. `:mixed` can be given for mixed case
  characters.

  ## Examples

      iex> Base.decode32("MZXW6YTBOI======")
      {:ok, "foobar"}

      iex> Base.decode32("mzxw6ytboi======", case: :lower)
      {:ok, "foobar"}

      iex> Base.decode32("mzXW6ytBOi======", case: :mixed)
      {:ok, "foobar"}

  """
  @spec decode32(binary) :: {:ok, binary} | :error
  @spec decode32(binary, Keyword.t) :: {:ok, binary} | :error
  def decode32(string, opts \\ []) do
    case = Keyword.get(opts, :case, :upper)
    {:ok, do_decode32(string, decode_case(case, &dec32/1))}
  rescue
    ArgumentError -> :error
  end

  @doc """
  Decodes a base 32 encoded string into a binary string.

  Accepts an atom `:upper` (default) for decoding from upper case characters or
  `:lower` for lower case characters. `:mixed` can be given for mixed case
  characters.

  An `ArgumentError` exception is raised if the padding is incorrect or
  a non-alphabet character is present in the string.

  ## Examples

      iex> Base.decode32!("MZXW6YTBOI======")
      "foobar"

      iex> Base.decode32!("mzxw6ytboi======", case: :lower)
      "foobar"

      iex> Base.decode32!("mzXW6ytBOi======", case: :mixed)
      "foobar"

  """
  @spec decode32!(binary) :: binary
  @spec decode32!(binary, Keyword.t) :: binary
  def decode32!(string, opts \\ []) do
    case = Keyword.get(opts, :case, :upper)
    do_decode32(string, decode_case(case, &dec32/1))
  end

  @doc """
  Encodes a binary string into a base 32 encoded string with an
  extended hexadecimal alphabet.

  Accepts an atom `:upper` (default) for encoding to upper case characters or
  `:lower` for lower case characters.

  ## Examples

      iex> Base.hex_encode32("foobar")
      "CPNMUOJ1E8======"

      iex> Base.hex_encode32("foobar", case: :lower)
      "cpnmuoj1e8======"

  """
  @spec hex_encode32(binary) :: binary
  @spec hex_encode32(binary, Keyword.t) :: binary
  def hex_encode32(data, opts \\ []) when is_binary(data) do
    case = Keyword.get(opts, :case, :upper)
    do_encode32(data, encode_case(case, &enc32hex/1))
  end

  @doc """
  Decodes a base 32 encoded string with extended hexadecimal alphabet
  into a binary string.

  Accepts an atom `:upper` (default) for decoding from upper case characters or
  `:lower` for lower case characters. `:mixed` can be given for mixed case
  characters.

  ## Examples

      iex> Base.hex_decode32("CPNMUOJ1E8======")
      {:ok, "foobar"}

      iex> Base.hex_decode32("cpnmuoj1e8======", case: :lower)
      {:ok, "foobar"}

      iex> Base.hex_decode32("cpnMuOJ1E8======", case: :mixed)
      {:ok, "foobar"}

  """
  @spec hex_decode32(binary) :: {:ok, binary} | :error
  @spec hex_decode32(binary, Keyword.t) :: {:ok, binary} | :error
  def hex_decode32(string, opts \\ []) when is_binary(string) do
    case = Keyword.get(opts, :case, :upper)
    {:ok, do_decode32(string, decode_case(case, &dec32hex/1))}
  rescue
    ArgumentError -> :error
  end

  @doc """
  Decodes a base 32 encoded string with extended hexadecimal alphabet
  into a binary string.

  Accepts an atom `:upper` (default) for decoding from upper case characters or
  `:lower` for lower case characters. `:mixed` can be given for mixed case
  characters.

  An `ArgumentError` exception is raised if the padding is incorrect or
  a non-alphabet character is present in the string.

  ## Examples

      iex> Base.hex_decode32!("CPNMUOJ1E8======")
      "foobar"

      iex> Base.hex_decode32!("cpnmuoj1e8======", case: :lower)
      "foobar"

      iex> Base.hex_decode32!("cpnMuOJ1E8======", case: :mixed)
      "foobar"

  """
  @spec hex_decode32!(binary) :: binary
  @spec hex_decode32!(binary, Keyword.t) :: binary
  def hex_decode32!(string, opts \\ []) when is_binary(string) do
    case = Keyword.get(opts, :case, :upper)
    do_decode32(string, decode_case(case, &dec32hex/1))
  end

  defp do_encode16(<<>>, _), do: <<>>
  defp do_encode16(data, enc) do
    for <<c::4 <- data>>, into: <<>>, do: <<enc.(c)::8>>
  end

  defp do_decode16(<<>>, _), do: <<>>
  defp do_decode16(string, dec) when rem(byte_size(string), 2) == 0 do
    for <<c1::8, c2::8 <- string>>, into: <<>>  do
      <<dec.(c1)::4, dec.(c2)::4>>
    end
  end
  defp do_decode16(_, _) do
    raise ArgumentError, "odd-length string"
  end

  defp do_encode64(<<>>, _), do: <<>>
  defp do_encode64(data, enc) do
    split =  3 * div(byte_size(data), 3)
    <<main::size(split)-binary, rest::binary>> = data
    main = for <<c::6 <- main>>, into: <<>>, do: <<enc.(c)::8>>
    case rest do
      <<c1::6, c2::6, c3::4>> ->
        <<main::binary, enc.(c1)::8, enc.(c2)::8, enc.(bsl(c3, 2))::8, ?=>>
      <<c1::6, c2::2>> ->
        <<main::binary, enc.(c1)::8, enc.(bsl(c2, 4))::8, ?=, ?=>>
      <<>> ->
        main
    end
  end

  defp do_decode64(<<>>, _), do: <<>>
  defp do_decode64(string, dec) when rem(byte_size(string), 4) == 0 do
    split = byte_size(string) - 4
    <<main::size(split)-binary, rest::binary>> = string
    main = for <<c::8 <- main>>, into: <<>>, do: <<dec.(c)::6>>
    case rest do
      <<c1::8, c2::8, ?=, ?=>> ->
        <<main::binary, dec.(c1)::6, bsr(dec.(c2), 4)::2>>
      <<c1::8, c2::8, c3::8, ?=>> ->
        <<main::binary, dec.(c1)::6, dec.(c2)::6, bsr(dec.(c3), 2)::4>>
      <<c1::8, c2::8, c3::8, c4::8>> ->
        <<main::binary, dec.(c1)::6, dec.(c2)::6, dec.(c3)::6, dec.(c4)::6>>
      <<>> ->
        main
    end
  end
  defp do_decode64(_, _) do
    raise ArgumentError, "incorrect padding"
  end

  defp do_encode32(<<>>, _), do: <<>>
  defp do_encode32(data, enc) do
    split =  5 * div(byte_size(data), 5)
    <<main::size(split)-binary, rest::binary>> = data
    main = for <<c::5 <- main>>, into: <<>>, do: <<enc.(c)::8>>
    case rest do
      <<c1::5, c2::5, c3::5, c4::5, c5::5, c6::5, c7::2>> ->
        <<main::binary,
          enc.(c1)::8, enc.(c2)::8, enc.(c3)::8, enc.(c4)::8,
          enc.(c5)::8, enc.(c6)::8, enc.(bsl(c7, 3))::8, ?=>>
      <<c1::5, c2::5, c3::5, c4::5, c5::4>> ->
        <<main::binary,
          enc.(c1)::8, enc.(c2)::8, enc.(c3)::8, enc.(c4)::8,
          enc.(bsl(c5, 1))::8, ?=,  ?=, ?=>>
      <<c1::5, c2::5, c3::5, c4::1>> ->
        <<main::binary,
          enc.(c1)::8, enc.(c2)::8,  enc.(c3)::8, enc.(bsl(c4, 4))::8,
          ?=, ?=,  ?=, ?=>>
      <<c1::5, c2::3>> ->
        <<main::binary,
          enc.(c1)::8, enc.(bsl(c2, 2))::8, ?=, ?=,
          ?=, ?=, ?=, ?=>>
      <<>> ->
        main
    end
  end

  defp do_decode32(<<>>, _), do: <<>>
  defp do_decode32(string, dec) when rem(byte_size(string), 8) == 0 do
    split = byte_size(string) - 8
    <<main::size(split)-binary, rest::binary>> = string
    main = for <<c::8 <- main>>, into: <<>>, do: <<dec.(c)::5>>
    case rest do
      <<c1::8, c2::8, ?=, ?=, ?=, ?=, ?=, ?=>> ->
        <<main::binary, dec.(c1)::5, bsr(dec.(c2), 2)::3>>
      <<c1::8, c2::8, c3::8, c4::8, ?=, ?=, ?=, ?=>> ->
        <<main::binary,
          dec.(c1)::5, dec.(c2)::5, dec.(c3)::5, bsr(dec.(c4), 4)::1>>
      <<c1::8, c2::8, c3::8, c4::8, c5::8, ?=, ?=, ?=>> ->
        <<main::binary,
          dec.(c1)::5, dec.(c2)::5, dec.(c3)::5, dec.(c4)::5,
          bsr(dec.(c5), 1)::4>>
      <<c1::8, c2::8, c3::8, c4::8, c5::8, c6::8, c7::8, ?=>> ->
        <<main::binary,
          dec.(c1)::5, dec.(c2)::5, dec.(c3)::5, dec.(c4)::5,
          dec.(c5)::5, dec.(c6)::5,  bsr(dec.(c7), 3)::2>>
      <<c1::8, c2::8, c3::8, c4::8, c5::8, c6::8, c7::8, c8::8>> ->
        <<main::binary,
          dec.(c1)::5, dec.(c2)::5, dec.(c3)::5, dec.(c4)::5,
          dec.(c5)::5, dec.(c6)::5, dec.(c7)::5, dec.(c8)::5>>
      <<>> ->
        main
    end
  end
  defp do_decode32(_, _) do
    raise ArgumentError, "incorrect padding"
  end

end
