# frozen_string_literal: true

# This file is part of PacketGen
# See https://github.com/lemontree55/packetgen for more informations
# Copyright (C) 2016 Sylvain Daubert <sylvain.daubert@laposte.net>
# Copyright (C) 2024 LemonTree55 <lenontree@proton.me>
# This program is published under MIT license.

module PacketGen
  module Header
    # This class supports IGMPv3 (RFC3376).
    #
    # From RFC 3376, a IGMP header has the following format:
    #   0                   1                   2                   3
    #   0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
    #   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    #   |      Type     | Max Resp Code |           Checksum            |
    #   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    #
    # A IGMP header consists of:
    # * a {#type #type} field (+BinStruct::Int8Enum+ type),
    # * a {#max_resp_time #max_resp_time} field (+BinStruct::Int8+ type),
    # * a {#checksum #checksum} field (+BinStruct::Int16+ type),
    # * and a {#body #body}, containing more fields (see below).
    #
    # A IGMPv3 header may have additionnal fields. These attributes are handled by
    # additional headers (see {IGMPv3::MQ}).
    #
    # @example Create a IGMPv3 header
    #  # standalone
    #  igmp = PacketGen::Header::IGMPv3.new
    #  # in a packet
    #  pkt = PacketGen.gen('IP').add('IGMPv3')
    #  # access to IGMPv3 header
    #  pkt.igmpv3.class  # => PacketGen::Header::IGMPv3
    #
    # @example IGMPv3 attributes
    #  igmp = PacketGen::Header::IGMPv3.new
    #  igmp.type = 'MembershipQuery'   # or 0x11
    #  igmp.max_resp_time = 20
    #  igmp.checksum = 0x248a
    #
    # == IGMPv3 specifics
    # === Max Resp Code
    # {#max_resp_code} field of IGMPv3 packets is encoded differently than
    # previous versions. This encoding permits to set value up to 31743 (instead
    # of 255 for IGMPv2).
    #
    # This encoding is handled by {#max_resp_code} accessors:
    #   igmp.max_resp_code = 10000
    #   igmp.max_resp_code   #=> 9728  error due to encoding as a floating point value
    #
    # === IGMPv3 Membership Query
    # With IGMPv3, a Membership Query packet has more attributes than with IGMPv2. To
    # handle those fields, an additional header should be used:
    #   pkt = PacketGen.gen('IP').add('IGMPv3', type: 'MembershipQuery').add('IGMPv3::MQ')
    #   pkt.igmpv3      #=> PacketGen::Header::IGMPv3
    #   pkt.igmpv3_mq   #=> PacketGen::Header::IGMPv3::MQ
    #
    # === IGMPv3 Membership Report
    # With IGMPv3, a Membership Report packet has more attributes than with IGMPv2. To
    # handle those fields, an additional header should be used:
    #   pkt =  PacketGen.gen('IP').add('IGMPv3', type: 'MembershipQuery').add('IGMPv3::MR')
    #   pkt.igmpv3      #=> PacketGen::Header::IGMPv3
    #   pkt.igmpv3_mr  #=> PacketGen::Header::IGMPv3::MR
    # @author Sylvain Daubert
    # @since 2.4.0
    class IGMPv3 < IGMP
      # Known types
      TYPES = {
        'MembershipQuery' => 0x11,
        'MembershipReport' => 0x22,
      }.freeze

      remove_attr :group_addr

      # Encode value for IGMPv3 Max Resp Code and QQIC.
      # Value may be encoded as a float, so some error may occur.
      # See RFC 3376 §4.1.1 and §4.1.7.
      # @param [Integer] value
      # @return [Integer]
      def self.encode(value)
        if value < 128
          value
        elsif value > 31_743
          255
        else
          exp = 0
          value >>= 3
          while value > 31
            exp += 1
            value >>= 1
          end
          0x80 | (exp << 4) | (value & 0xf)
        end
      end

      # Decode value for IGMPv3 Max Resp Code and QQIC.
      # See RFC 3376 §4.1.1 and §4.1.7.
      # @param [Integer] value
      # @return [Integer]
      def self.decode(value)
        if value < 128
          value
        else
          mant = value & 0xf
          exp = (value >> 4) & 0x7
          (0x10 | mant) << (exp + 3)
        end
      end

      # Getter for +max_resp_time+ for IGMPv3 packets. Use {.decode}.
      # @return [Integer]
      def max_resp_time
        IGMPv3.decode(self[:max_resp_time].value || self[:max_resp_time].default)
      end
      alias max_resp_code max_resp_time

      # Setter for +max_resp_time+ for IGMPv3 packets. Use {.encode}.
      # @param [Integer] value
      # @return [Integer]
      def max_resp_time=(value)
        self[:max_resp_time].value = IGMPv3.encode(value)
      end
      alias max_resp_code= max_resp_time=

      # Compute checksum and set +checksum+ field
      # @return [Integer]
      def calc_checksum
        sum = IP.sum16(self)
        self.checksum = IP.reduce_checksum(sum)
      end
    end

    self.add_class IGMPv3
    IP.bind IGMPv3, protocol: IGMPv3::IP_PROTOCOL, frag: 0, ttl: 1, tos: 0xc0
  end
end

require_relative 'igmpv3/mq'
require_relative 'igmpv3/mr'
