defmodule OpenPGP do
  @v05x_note """
  As of 0.5.x subset of RFC4880 Packet Tags can be casted. Other Packet
  tags remain as %Packet{} (not casted). Should not be considered as
  error.
  """

  @moduledoc """
  OpenPGP lib allows to inspect, decode and decrypt OpenPGP Message
  Format as per [RFC4880](https://www.ietf.org/rfc/rfc4880.txt)

  As of v0.5.x:

    1. Any valid OpenPGP message can be decoded via generic
       `OpenPGP.Packet` decoder. This abstraction layer provide Packet
       Tags and Body Chunks for packet envelope level evaluation.
    1. Some Packet Tag specific decoders implemented with limited
       feature support:
        1. `OpenPGP.LiteralDataPacket`
        1. `OpenPGP.PublicKeyEncryptedSessionKeyPacket`
        1. `OpenPGP.PublicKeyPacket` - support only V4 packets
        1. `OpenPGP.SecretKeyPacket` - support only V4 packets; Iterated
            and Salted String-to-Key (S2K) specifier (ID: 3); S2K usage
            convention octet of 254 only; S2K hashing algo SHA1; AES128
            symmetric encryption of secret key material
        1. `OpenPGP.CompressedDataPacket` - support only ZLIB- and ZIP-
            style blocks
        1. `OpenPGP.IntegrityProtectedDataPacket` - support Session Key
            algo 9 (AES with 256-bit key) in CFB mode; Modification
            Detection Code system is not supported

  At a high level `OpenPGP.list_packets/1` and `OpenPGP.cast_packets/1`
  serve as an entrypoint to OpenPGP Message decoding and extracting
  generic data. Packet specific decoders implement
  `OpenPGP.Packet.Behaviour`, which exposes `.decode/1` interface
  (including genric `OpenPGP.Packet`). Additionaly some of the packet
  specific decoders may provide interface for further packet processing,
  such as `OpenPGP.SecretKeyPacket.decrypt/2`.

  ### Examples:

  Decode message packets and then cast

      iex> message = <<160, 24, 2, 120, 156, 243, 72, 205, 201, 201, 215, 81, 8, 207, 47, 202, 73,
      ...>     81, 84, 84, 4, 0, 40, 213, 4, 172>>
      ...>
      iex> packets = OpenPGP.list_packets(message)
      [
        %OpenPGP.Packet{
          body: [
            %OpenPGP.Packet.BodyChunk{
              chunk_length: {:fixed, 24},
              data: <<2, 120, 156, 243, 72, 205, 201, 201, 215, 81, 8, 207, 47, 202, 73, 81, 84,
                  84, 4, 0, 40, 213, 4, 172>>,
              header_length: 1
            }
          ],
          tag: %OpenPGP.Packet.PacketTag{
            format: :old,
            length_type: {0, "one-octet"},
            tag: {8, "Compressed Data Packet"}
          }
        }
      ]
      iex> OpenPGP.cast_packets(packets)
      [
        %OpenPGP.CompressedDataPacket{
          algo: {2, "ZLIB [RFC1950]"},
          data_deflated: <<120, 156, 243, 72, 205, 201, 201, 215, 81, 8, 207, 47, 202, 73, 81, 84,
              84, 4, 0, 40, 213, 4, 172>>,
          data_inflated: "Hello, World!!!"
        }
      ]
  """

  alias __MODULE__.Encode
  alias __MODULE__.Encrypt
  alias __MODULE__.Packet
  alias __MODULE__.Packet.PacketTag
  alias __MODULE__.Util

  @type any_packet ::
          %OpenPGP.Packet{}
          | %OpenPGP.PublicKeyEncryptedSessionKeyPacket{}
          | %OpenPGP.SecretKeyPacket{}
          | %OpenPGP.PublicKeyPacket{}
          | %OpenPGP.CompressedDataPacket{}
          | %OpenPGP.IntegrityProtectedDataPacket{}
          | %OpenPGP.LiteralDataPacket{}
          | %OpenPGP.ModificationDetectionCodePacket{}

  @doc """
  Decode all packets in a message (input).
  Return a list of %Packet{} structs. Does not cast packets. To cast
  generic packets, use `.cast_packets/1` after `.list_packets/1`, i.e.
  <<...>> |> OpenPGP.list_packets() |> OpenPGP.cast_packets()

  This function extremely handy for inspection, when operating at PTag
  and BodyChunk level.
  """
  @spec list_packets(binary()) :: [Packet.t()]
  def list_packets("" <> _ = input), do: do_list_packets(input, [])

  @spec do_list_packets(input :: binary(), acc :: [Packet.t()]) :: [Packet.t()]
  defp do_list_packets("", acc), do: Enum.reverse(acc)

  defp do_list_packets("" <> _ = input, acc) do
    {packet, next} = Packet.decode(input)
    do_list_packets(next, [packet | acc])
  end

  @doc """
  Similar to `.cast_packet/1`, but operates on a list of generic
  packets.

  > NOTE: #{@v05x_note}
  """
  @spec cast_packets([Packet.t()]) :: [any_packet()]
  def cast_packets(packets) when is_list(packets), do: Enum.map(packets, &cast_packet/1)

  @tag_to_packet %{
    1 => OpenPGP.PublicKeyEncryptedSessionKeyPacket,
    5 => OpenPGP.SecretKeyPacket,
    6 => OpenPGP.PublicKeyPacket,
    7 => OpenPGP.SecretKeyPacket,
    8 => OpenPGP.CompressedDataPacket,
    11 => OpenPGP.LiteralDataPacket,
    14 => OpenPGP.PublicKeyPacket,
    18 => OpenPGP.IntegrityProtectedDataPacket
  }
  @tag_to_packet_ids Map.keys(@tag_to_packet)

  @doc """
  Cast a generic packet %Packet{} to a speicific struct with a packet
  specific data assigned.

  > NOTE: #{@v05x_note}
  """
  @spec cast_packet(Packet.t()) :: any_packet()
  def cast_packet(%Packet{} = packet) do
    case packet.tag do
      %PacketTag{tag: {tag_id, _}} when tag_id in @tag_to_packet_ids ->
        impl = Map.get(@tag_to_packet, tag_id)
        {casted, <<>>} = packet |> Util.concat_body() |> impl.decode()
        casted

      _ ->
        packet
    end
  end

  @doc "Encode any packet (except for %Packet{}) that implements `OpenPGP.Encode` protocol."
  @spec encode_packet(any_packet()) :: binary()
  def encode_packet(%{} = packet) do
    tag = Encode.tag(packet)
    ptag = %PacketTag{format: :new, tag: tag}
    body = Encode.encode(packet)

    Encode.encode(%Packet{tag: ptag, body: body})
  end

  @doc "Encrypt any packet that implements `OpenPGP.Encrypt` protocol."
  @spec encrypt_packet(packet, opts :: Keyword.t()) :: packet when packet: any_packet()
  def encrypt_packet(%{} = packet, opts \\ []) when is_list(opts), do: Encrypt.encrypt(packet, opts)
end
