defmodule Text.Corpus do
  @moduledoc """
  Defines the behaviour for a language
  corpus with convenience functions to
  simplifying the creation of corpus
  vocabularies.

  """

  @doc """
  Returns a list of vocabularies for a corpus.

  """
  @callback known_vocabularies :: [Text.vocabulary, ...]

  @doc """
  Returns a ist of vocabularies for a corpus.

  """
  @callback known_languages :: [Text.language, ...]

  @doc """
  Returns the natural langauge training text for
  a given language in the corpus.

  """
  @callback language_content(Text.language) :: String.t

  @doc """
  Normalizes the text used for training and
  for classification.

  """
  @callback normalize_text(String.t) :: String.t

  @doc """
  Classifies the natural language of a given
  text into an ordered list.

  See `Text.Language.classify/2` for
  the options that may be passed.

  """
  @callback classify(String.t, Keyword.t) ::
    [Text.frequency_tuple, ...] | {:error, {module(), String.t}}

  @doc """
  Detects the most likely natural language of a given
  text.

  See `Text.Language.detect/2` for
  the options that may be passed.

  """
  @callback detect(String.t, Keyword.t) ::
    {:ok, Text.language} | {:error, {module(), String.t}}

  @max_demand 5

  @doc """
  Builds the vocabulary for
  all known vocabulary modules

  """
  def build_vocabularies(corpus, options \\ []) do
    max_demand = Keyword.get(options, :max_demand, @max_demand)

    corpus.known_vocabularies()
    |> Enum.each(&build_vocabulary(corpus, &1, max_demand: max_demand))
  end

  @doc """
  Builds a vocabulary for a given vocanulary
  module.

  """
  def build_vocabulary(corpus, vocabulary, options \\ []) do
    ngram_range = vocabulary.ngram_range()
    file = vocabulary.filename()
    max_demand = Keyword.get(options, :max_demand, @max_demand)

    frequency_map_by_language =
      corpus.known_languages()
      |> Flow.from_enumerable(max_demand: max_demand)
      |> Flow.map(&Text.Vocabulary.calculate_corpus_ngrams(corpus, &1, ngram_range))
      |> Enum.to_list
      |> calculate_global_frequencies
      |> remove_structs_for_space_reduction

    binary = :erlang.term_to_binary(frequency_map_by_language)
    :ok = File.write!(file, binary)

    frequency_map_by_language
  end

  @doc false
  def remove_structs_for_space_reduction(frequency_map) do
    Enum.map(frequency_map, fn {language, ngram_map} ->
      new_ngram_map =
        Enum.map(ngram_map, fn {ngram, stats} -> {ngram, Map.from_struct(stats)} end)
        |> Map.new()

      {language, new_ngram_map}
    end)
    |> Map.new()
  end

  # Calculate the total frequency for each
  # ngram across all regions
  @doc false
  def calculate_global_frequencies(frequency_map_by_language) do
    frequency_map_by_language
    |> invert_to_frequency_map_by_ngram()
    |> calculate_global_frequency_and_rank()
    |> invert_to_frequency_map_by_language()
  end

  # Invert
  #   %{language => %{ngram => frequencies}}
  # to:
  #   %{ngram => %{language => frequencies}}
  @doc false
  def invert_to_frequency_map_by_ngram(frequency_map_by_language) do
    Enum.reduce(frequency_map_by_language, %{}, fn {language, ngrams}, acc ->
      Enum.reduce(ngrams, acc, fn {ngram, ngram_stats}, acc2 ->
        Map.update(acc2, ngram, %{language => ngram_stats}, &Map.put(&1, language, ngram_stats))
      end)
    end)
  end

  # Invert
  #   %{ngram => %{language => frequencies}}
  # to:
  #   %{language => %{ngram => frequencies}}
  @doc false
  def invert_to_frequency_map_by_language(frequency_map_by_ngram) do
    Enum.reduce(frequency_map_by_ngram, %{}, fn {ngram, languages}, acc ->
      Enum.reduce(languages, acc, fn {language, ngram_stats}, acc2 ->
        Map.update(acc2, language, %{ngram => ngram_stats}, &Map.put(&1, ngram, ngram_stats))
      end)
    end)
  end

  # Calculate the frequencies across all regions
  # and then the global range across all region
  @doc false
  def calculate_global_frequency_and_rank(frequency_map_by_ngram) do
    Enum.map(frequency_map_by_ngram, fn {ngram, ngram_by_language} ->
      total_count_for_ngram = total_ngram_count_for_languages(ngram_by_language)

      added_global_stats =
        Enum.map(ngram_by_language, fn {language, ngram_stats} ->
          {language, %{ngram_stats | global_frequency: ngram_stats.count / total_count_for_ngram}}
        end)
        |> Enum.sort(&(elem(&1, 1).global_frequency > elem(&2, 1).global_frequency))
        |> Enum.with_index(1)
        |> Enum.map(fn {{language, ngram_stats}, global_rank} ->
          {language, %{ngram_stats | global_rank: global_rank}}
        end)

      {ngram, added_global_stats}
    end)
  end

  @doc false
  def total_ngram_count_for_languages(ngram_by_language) do
    Enum.reduce(ngram_by_language, 0, fn {_language, %{count: count}}, acc ->
      acc + count
    end)
  end
end