lib/gpp.ex

defmodule Gpp do
  @moduledoc """
  IAB Global Privacy Platform string decoding.

  [Spec](https://github.com/biteractiveAdvertisingBureau/Global-Privacy-Platform)
  """
  alias Gpp.{Sections, SectionRange, IdRange, FibonacciDecoder, BitUtil}
  alias Gpp.Sections.{Uspca, Uspco, Uspct, Usput, Uspv1, Uspva, Uspnat, Tcf}

  @type section ::
          Tcf.t()
          | Uspca.t()
          | Uspco.t()
          | Uspct.t()
          | Usput.t()
          | Uspv1.t()
          | Uspva.t()
          | Uspnat.t()
  @type section_id :: pos_integer()
  @type t :: %__MODULE__{
          type: pos_integer(),
          version: pos_integer(),
          section_ids: [section_id()],
          sections: [section()]
        }

  defstruct type: 3, version: 1, section_ids: [], sections: []

  defmodule InvalidHeader do
    defexception [:message]
  end

  defmodule InvalidType do
    defexception [:message]
  end

  defmodule InvalidVersion do
    defexception [:message]
  end

  defmodule InvalidSectionRange do
    defexception [:message]
  end

  defmodule UnknownSection do
    defexception [:message]
  end

  defmodule DeprecatedSection do
    defexception [:section_id, message: "has been deprecated"]
  end

  @min_header_length 3

  @sections %{
    2 => {"tcfeu2", &Sections.Tcf.parse/1},
    3 => {"gpp header", nil},
    5 => {"tcfcav1", &Sections.Tcfcav1.parse/1},
    6 => {"uspv1", &Sections.Uspv1.parse/1},
    7 => {"uspnat", &Sections.Uspnat.parse/1},
    8 => {"uspca", &Sections.Uspca.parse/1},
    9 => {"uspva", &Sections.Uspva.parse/1},
    10 => {"uspco", &Sections.Uspco.parse/1},
    11 => {"usput", &Sections.Usput.parse/1},
    12 => {"uspct", &Sections.Uspct.parse/1}
  }

  @spec parse(String.t()) :: {:ok, t()} | {:error, Exception.t()}
  def parse(input) when byte_size(input) > 3 do
    [header | sections] = String.split(input, "~")

    with :ok <- validate_header_length(header),
         {:ok, header_bits} <- BitUtil.url_base64_to_bits(header),
         {:ok, gpp, section_range} <- parse_header(header_bits) do
      parse_sections(gpp, section_range, sections)
    end
  end

  def parse(invalid), do: {:error, %InvalidHeader{message: "got: #{inspect(invalid)}"}}

  for {sid, {name, _}} <- @sections do
    def section_id_to_name!(unquote(sid)), do: unquote(name)
    def section_name_to_id!(unquote(name)), do: unquote(sid)
  end

  def section_id_to_name!(unknown) do
    raise UnknownSection, "got id: #{inspect(unknown)}"
  end

  def section_name_to_id!(unknown) do
    raise UnknownSection, "got name: #{inspect(unknown)}"
  end

  defp validate_header_length(header) when byte_size(header) > @min_header_length, do: :ok

  defp validate_header_length(header) do
    {:error,
     %InvalidHeader{
       message: "header must be atleast #{@min_header_length} bytes long, got: #{inspect(header)}"
     }}
  end

  defp parse_header(bits) do
    with {:ok, type, version_and_section_range} <- type(bits),
         {:ok, version, rest} <- version(version_and_section_range),
         {:ok, section_range} <- section_range(rest) do
      {:ok, %__MODULE__{type: type, version: version}, section_range}
    end
  end

  defp parse_sections(gpp, section_range, sections) do
    with {:ok, section_ids, sections} <-
           sections(section_range, sections) do
      {:ok, %{gpp | section_ids: section_ids, sections: sections}}
    end
  end

  defp type(input) do
    case BitUtil.parse_6bit_int(input) do
      {:ok, type, rest} when type == 3 ->
        {:ok, type, rest}

      {:ok, type, _} ->
        {:error, %InvalidType{message: "must equal 3, got #{inspect(type)}"}}

      _ ->
        {:error, %InvalidHeader{message: "got #{inspect(input)}"}}
    end
  end

  defp version(input) do
    with {:ok, version, rest} <- BitUtil.parse_6bit_int(input) do
      {:ok, version, rest}
    end
  end

  defp section_range(input), do: decode_section_range(input)

  defp decode_section_range(input) do
    with {:ok, section_count, rest} <- BitUtil.parse_12bit_int(input) do
      decode_fibonacci_range(section_count, rest)
    end
  end

  defp decode_fibonacci_range(count, input) do
    decode_fibonacci_range(count, input, %SectionRange{size: count})
  end

  defp decode_fibonacci_range(0, _rest, %{range: range} = acc) do
    {:ok, %{acc | range: Enum.reverse(range)}}
  end

  defp decode_fibonacci_range(count, [0 | input], acc) do
    with {:ok, {offset, rest}} <- decode_fibonacci_word(input) do
      entry = acc.max + offset
      id_range = %IdRange{start_id: entry, end_id: entry}
      acc = %{acc | max: max(entry, acc.max), range: [id_range | acc.range]}
      decode_fibonacci_range(count - 1, rest, acc)
    end
  end

  defp decode_fibonacci_range(count, [1 | input], acc) do
    with {:ok, {offset, rest}} <- decode_fibonacci_word(input),
         {:ok, {second_offset, rest}} <- decode_fibonacci_word(rest) do
      start_id = acc.max + offset
      end_id = start_id + second_offset
      id_range = %IdRange{start_id: start_id, end_id: end_id}
      acc = %{acc | max: max(end_id, acc.max), range: [id_range | acc.range]}
      decode_fibonacci_range(count - 1, rest, acc)
    end
  end

  defp decode_fibonacci_word(input, acc \\ [])

  defp decode_fibonacci_word([], acc) do
    {:error, %InvalidSectionRange{message: "got #{inspect(Enum.reverse(acc))}"}}
  end

  # fibonacci code words are variable length, but always end in 1,1
  defp decode_fibonacci_word([1, 1 | rest], acc) do
    full_word = Enum.reverse([1, 1 | acc])

    with {:ok, next} <- FibonacciDecoder.decode(full_word) do
      {:ok, {next, rest}}
    end
  end

  defp decode_fibonacci_word([next | rest], acc), do: decode_fibonacci_word(rest, [next | acc])

  defp sections(section_range, input) do
    section_ids =
      Enum.flat_map(section_range.range, fn %{start_id: start_id, end_id: end_id} ->
        for i <- start_id..end_id, do: i
      end)

    input_with_ids = Enum.zip(input, section_ids)

    with {:ok, sections} <- parse_sections(input_with_ids) do
      {:ok, section_ids, Enum.reverse(sections)}
    end
  end

  defp parse_sections(input_with_ids) do
    Enum.reduce_while(input_with_ids, {:ok, []}, fn {value, id}, {:ok, acc} ->
      with {:ok, parser} <- parser(id),
           {:ok, parsed} <- parser.(value) do
        {:cont, {:ok, [%{parsed | section_id: id} | acc]}}
      else
        e ->
          {:halt, e}
      end
    end)
  end

  for {id, {_, fun}} when is_function(fun) <- @sections do
    defp parser(unquote(id)), do: {:ok, unquote(fun)}
  end

  defp parser(id), do: {:error, %DeprecatedSection{section_id: id}}
end