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}

  @type section_id :: pos_integer()
  @type t :: %__MODULE__{
          type: pos_integer(),
          version: pos_integer(),
          section_ids: [section_id()],
          sections: []
        }

  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 DeprecatedSection do
    defexception [:id, message: "has been deprecated"]
  end

  @min_header_length 3

  @sections %{
    2 => {"tcfeu2", &Sections.Tcf.parse/1},
    3 => {"gpp header", nil},
    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, term()}
  def parse(input) 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

  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}

      other ->
        {:error, %InvalidType{message: "must equal 3, got #{other}"}}
    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, [next_bit | input], acc) do
    if next_bit == 0 do
      {offset, rest} = decode_fibonacci_word(input)
      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)
    else
      {offset, rest} = decode_fibonacci_word(input)
      {second_offset, rest} = decode_fibonacci_word(rest)
      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: {:ok, Enum.reverse(acc)}

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

    {next, rest}
  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)

    sections =
      Enum.zip_with(input, section_ids, fn value, id ->
        with {:ok, parser} <- parser(id),
             {:ok, parsed} <- parser.(value) do
          parsed
        else
          {:error, error} -> error
        end
      end)

    {:ok, section_ids, sections}
  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{id: id}}
end