lib/exiffer/jpeg/header/Data.ex

defmodule Exiffer.JPEG.Header.Data do
  @moduledoc """
  Documentation for `Exiffer.JPEG.Header.Data`.
  """

  require Logger

  alias Exiffer.Binary
  import Exiffer.Logging, only: [integer: 1]

  @enforce_keys ~w(type data)a
  defstruct ~w(type data)a

  defimpl Jason.Encoder do
    @spec encode(%Exiffer.JPEG.Header.Data{}, Jason.Encode.opts()) :: String.t()
    def encode(entry, opts) do
      Logger.debug("Encoding Data")

      Jason.Encode.map(
        %{
          module: "Exiffer.JPEG.Header.Data",
          type: entry.type,
          data: "(#{byte_size(entry.data)} bytes)"
        },
        opts
      )
    end
  end

  @data_type %{
    <<0xFF, 0xC0>> => %{key: :jpeg_sof0, name: "JPEG SOF0"},
    <<0xFF, 0xC2>> => %{key: :jpeg_sof2, name: "JPEG SOF2"},
    <<0xFF, 0xC4>> => %{key: :jpeg_dht, name: "JPEG DHT"},
    # Define Restart Interval
    <<0xFF, 0xDD>> => %{key: :jpeg_dri, name: "JPEG DRI"},
    <<0xFF, 0xDB>> => %{key: :jpeg_dqt, name: "JPEG DQT"},
    <<0xFF, 0xE0>> => %{key: :jpeg_jfxx, name: "JPEG JFXX"},
    <<0xFF, 0xE2>> => %{key: :jpeg_app2, name: "JPEG APP2"},
    <<0xFF, 0xE3>> => %{key: :jpeg_app3, name: "JPEG APP3"},
    <<0xFF, 0xE5>> => %{key: :jpeg_app5, name: "JPEG APP5"},
    <<0xFF, 0xE6>> => %{key: :jpeg_app6, name: "JPEG APP6"},
    <<0xFF, 0xE7>> => %{key: :jpeg_app7, name: "JPEG APP7"},
    <<0xFF, 0xE8>> => %{key: :jpeg_app8, name: "JPEG APP8"},
    <<0xFF, 0xE9>> => %{key: :jpeg_app9, name: "JPEG APP9"},
    <<0xFF, 0xEA>> => %{key: :jpeg_app10, name: "JPEG APP10"},
    <<0xFF, 0xEB>> => %{key: :jpeg_app11, name: "JPEG APP11"},
    <<0xFF, 0xEC>> => %{key: :jpeg_app12, name: "JPEG APP12"},
    <<0xFF, 0xED>> => %{key: :jpeg_app13, name: "JPEG APP13"},
    <<0xFF, 0xEE>> => %{key: :jpeg_app14, name: "JPEG APP14"},
    <<0xFF, 0xEF>> => %{key: :jpeg_app15, name: "JPEG APP15"},
    <<0xFF, 0xFE>> => %{key: :jpeg_comment, name: "JPEG COM Comment"}
  }

  @magic Enum.into(@data_type, %{}, fn {magic, %{key: key}} -> {key, magic} end)

  def new(%{} = buffer) do
    position = buffer.position

    {<<magic::binary-size(2), length_binary::binary-size(2)>>, buffer} =
      Exiffer.Buffer.consume(buffer, 4)

    type = @data_type[magic]

    if type do
      # TODO: is this really always big endian?
      length = Binary.big_endian_to_integer(length_binary)
      Logger.debug("Reading #{type.name} header at #{integer(position)}, length #{integer(length)}")
      {data, buffer} = Exiffer.Buffer.consume(buffer, length - 2)
      header = %__MODULE__{type: type.key, data: data}
      {:ok, header, buffer}
    else
      {:error, "Unknown header magic #{inspect(magic, base: :hex)} found at #{integer(position)}"}
    end
  end

  def binary(%__MODULE__{type: type, data: data}) do
    length = 2 + byte_size(data)
    binary_length = Binary.int16u_to_big_endian(length)

    <<
      @magic[type]::binary,
      binary_length::binary,
      data::binary
    >>
  end

  def text(%__MODULE__{type: type, data: data}) do
    length = byte_size(data)

    """
    Data
    ----
    type: #{type}
    data: #{length} bytes
    """
  end

  def write(%__MODULE__{type: type, data: data}, io_device) do
    Logger.debug("Writing generic data header, type: #{type}")
    magic = @magic[type]
    IO.binwrite(io_device, magic)
    length = 2 + byte_size(data)
    length_binary = Binary.int16u_to_big_endian(length)
    IO.binwrite(io_device, length_binary)
    IO.binwrite(io_device, data)
  end

  defimpl Exiffer.Serialize do
    alias Exiffer.JPEG.Header.Data

    def write(data, io_device) do
      Data.write(data, io_device)
    end

    def binary(data) do
      Data.binary(data)
    end

    def text(data) do
      Data.text(data)
    end
  end
end