defmodule Mnemonex.Coder do
use GenServer
import Bitwise
alias TheFuzz.Phonetic.MetaphoneAlgorithm, as: MPA
@moduledoc """
Mnemonex server process
"""
@doc """
Start a linked process
"""
def start_link(opts, name \\ :mnemonex) do
GenServer.start_link(__MODULE__, opts, name: name)
end
def init(opts) do
{:ok, Mnemonex.Config.init(opts)}
end
def handle_call({:encode, string}, _from, state),
do: {:reply, string |> gather_words(state, []) |> format_mnx(state), state}
def handle_call({:decode, string}, _from, state),
do: {:reply, string |> gather_numbers(state) |> words_to_bin(state.base_words, <<>>), state}
defp gather_numbers(string, state),
do:
string
|> String.downcase()
|> word_list
|> List.flatten()
|> Enum.map(fn w -> word_to_index(w, state) end)
defp word_list(s), do: Regex.scan(~r/[a-z]+/, s)
defp word_to_index(word, state) do
case Map.fetch(state.word_indices, word) do
{:ok, v} ->
v
:error ->
sounds_like = word |> MPA.compute()
word_to_index(Map.fetch!(state.metaphone_map, sounds_like), state)
end
end
defp right_size(x, c) when byte_size(x) >= c, do: binary_part(x, 0, c)
defp right_size(x, c) when byte_size(x) < c, do: right_size(x <> <<0>>, c)
defp multi_add([a], _base, acc), do: acc + a
defp multi_add([x | rest], base, acc),
do: multi_add(rest, base, x |> (&(acc + &1)).() |> (&(base * &1)).())
defp words_to_bin([], _state, acc), do: acc
defp words_to_bin([a, b, c | rest], base, acc) when c >= base,
do: words_to_bin(rest, base, acc <> make_binary([c - base, b, a], 3, base))
defp words_to_bin([a, b, c | rest], base, acc) when c < base,
do: words_to_bin(rest, base, acc <> make_binary([c, b, a], 4, base))
defp words_to_bin([a, b | rest], base, acc),
do: words_to_bin(rest, base, acc <> make_binary([b, a], 2, base))
defp words_to_bin([a | rest], base, acc),
do: words_to_bin(rest, base, acc <> make_binary([a], 1, base))
def make_binary(list, c, base),
do: list |> multi_add(base, 0) |> :binary.encode_unsigned(:little) |> right_size(c)
defp word_x([], _s, acc), do: acc
defp word_x([a | rest], s, acc), do: word_x(rest, s + 8, acc ||| bsl(a, s))
defp to_words(_x, 0, _base, acc), do: acc
defp to_words(x, 1, base, acc), do: to_words(x, 0, base, [x |> rem(base) | acc])
defp to_words(x, 2, base, acc), do: to_words(x, 1, base, [x |> div(base) |> rem(base) | acc])
defp short_final_word(x, base, extra),
do: to_words(x, 2, base, [base + (x |> div(base) |> div(base) |> rem(base) |> rem(extra))])
defp long_final_word(x, base),
do: to_words(x, 2, base, [x |> div(base) |> div(base) |> rem(base)])
defp gather_words(<<>>, state, acc),
do: acc |> Enum.reverse() |> List.flatten() |> Enum.map(fn n -> elem(state.words, n) end)
defp gather_words(<<a, b, c, d, rest::binary>>, state, acc) do
words = [a, b, c, d] |> word_x(0, 0) |> long_final_word(state.base_words)
gather_words(rest, state, [words | acc])
end
defp gather_words(<<a, b, c>>, state, acc) do
words = [a, b, c] |> word_x(0, 0) |> short_final_word(state.base_words, state.rem_words)
gather_words(<<>>, state, [words | acc])
end
defp gather_words(<<a, b>>, state, acc) do
words = [a, b] |> word_x(0, 0) |> to_words(2, state.base_words, [])
gather_words(<<>>, state, [words | acc])
end
defp gather_words(<<a>>, state, acc) do
words = [a] |> word_x(0, 0) |> to_words(1, state.base_words, [])
gather_words(<<>>, state, [words | acc])
end
defp format_mnx(words, state) do
if state.as_list do
words
else
words
|> combine(state.words_per_group, state.word_sep, [])
|> combine(state.groups_per_line, state.group_sep, [])
|> final_output(state)
end
end
defp combine([], _c, _s, acc), do: acc |> Enum.reverse()
defp combine(bits, count, sep, acc) do
{these, rest} = Enum.split(bits, count)
combine(rest, count, sep, [Enum.join(these, sep) | acc])
end
defp final_output(lines, state),
do:
state.line_prefix <>
Enum.join(lines, state.line_suffix <> state.line_prefix) <> state.line_suffix
end