require 'ipaddr'
require_relative 'names'
require_relative 'rr/types'
require_relative 'rr/classes'

%w[a aaaa cname hinfo mr mx ns ptr soa srv txt].each do |file|
  require_relative "rr/#{file}"
end

module Net
  module DNS
    #
    # = Net::DNS::RR - DNS Resource Record class
    #
    # The Net::DNS::RR is the base class for DNS Resource
    # Record (RR) objects. A RR is a pack of data that represents
    # resources for a DNS zone. The form in which this data is
    # shows can be drawed as follow:
    #
    #   "name  ttl  class  type  data"
    #
    # The +name+ is the name of the resource, like an canonical
    # name for an +A+ record (internet ip address). The +ttl+ is the
    # time to live, expressed in seconds. +type+ and +class+ are
    # respectively the type of resource (+A+ for ip addresses, +NS+
    # for nameservers, and so on) and the class, which is almost
    # always +IN+, the Internet class. At the end, +data+ is the
    # value associated to the name for that particular type of
    # resource record. An example:
    #
    #   # A record for IP address
    #   "www.example.com  86400  IN  A  172.16.100.1"
    #
    #   # NS record for name server
    #   "www.example.com  86400  IN  NS  ns.example.com"
    #
    # A new RR object can be created in 2 ways: passing a string
    # such the ones above, or specifying each field as the pair
    # of an hash. See the Net::DNS::RR.new method for details.
    #
    class RR
      include Names

      # Base error class.
      class Error < StandardError
      end

      # Error in parsing binary data, maybe from a malformed packet.
      class DataError < Error
      end

      # Regexp matching an RR string
      RR_REGEXP = Regexp.new("^\\s*(\\S+)\\s*(\\d+)?\\s+(" +
                               Net::DNS::RR::Classes.regexp +
                               "|CLASS\\d+)?\\s*(" +
                               Net::DNS::RR::Types.regexp +
                               "|TYPE\\d+)?\\s*(.*)$", Regexp::IGNORECASE)

      # Dimension of the sum of class, type, TTL and rdlength fields in a
      # RR portion of the packet, in bytes
      RRFIXEDSZ = 10

      # Create a new instance of Net::DNS::RR class, or an instance of
      # any of the subclass of the appropriate type.
      #
      # Argument can be a string or an hash. With a sting, we can pass
      # a RR resource record in the canonical format:
      #
      #   a     = Net::DNS::RR.new("foo.example.com. 86400 A 10.1.2.3")
      #   mx    = Net::DNS::RR.new("example.com. 7200 MX 10 mailhost.example.com.")
      #   cname = Net::DNS::RR.new("www.example.com 300 IN CNAME www1.example.com")
      #   txt   = Net::DNS::RR.new('baz.example.com 3600 HS TXT "text record"')
      #
      # Incidentally, +a+, +mx+, +cname+ and +txt+ objects will be instances of
      # respectively Net::DNS::RR::A, Net::DNS::RR::MX, Net::DNS::RR::CNAME and
      # Net::DNS::RR::TXT classes.
      #
      # The name and RR data are required; all other informations are optional.
      # If omitted, the +TTL+ defaults to 10800, +type+ default to +A+ and the RR class
      # defaults to +IN+.  Omitting the optional fields is useful for creating the
      # empty RDATA sections required for certain dynamic update operations.
      # All names must be fully qualified.  The trailing dot (.) is optional.
      #
      # The preferred method is however passing an hash with keys and values:
      #
      #   rr = Net::DNS::RR.new(
      #                 :name    => "foo.example.com",
      #                 :ttl     => 86400,
      #                 :cls     => "IN",
      #                 :type    => "A",
      #                 :address => "10.1.2.3"
      #         )
      #
      #   rr = Net::DNS::RR.new(
      #                 :name => "foo.example.com",
      #                 :rdata => "10.1.2.3"
      #         )
      #
      # Name and data are required; all the others fields are optionals like
      # we've seen before. The data field can be specified either with the
      # right name of the resource (+:address+ in the example above) or with
      # the generic key +:rdata+. Consult documentation to find the exact name
      # for the resource in each subclass.
      #
      def initialize(arg)
        instance = case arg
        when String
          new_from_string(arg)
        when Hash
          new_from_hash(arg)
        else
          raise ArgumentError, "Invalid argument, must be a RR string or an hash of values"
        end

        if @type.to_s == "ANY"
          @cls = Net::DNS::RR::Classes.new("IN")
        end

        build_pack
        set_type

        instance
      end

      # Return a new RR object of the correct type (like Net::DNS::RR::A
      # if the type is A) from a binary string, usually obtained from
      # network stream.
      #
      # This method is used when parsing a binary packet by the Packet
      # class.
      #
      def self.parse(data)
        o = allocate
        obj, offset = o.send(:new_from_binary, data, 0)
        obj
      end

      # Same as RR.parse, but takes an entire packet binary data to
      # perform name expansion. Default when analizing a packet
      # just received from a network stream.
      #
      # Return an instance of appropriate class and the offset
      # pointing at the end of the data parsed.
      #
      def self.parse_packet(data, offset)
        o = allocate
        o.send(:new_from_binary, data, offset)
      end

      attr_reader :name

      attr_reader :ttl

      # Type accessor
      def type
        @type.to_s
      end

      # Class accessor
      def cls
        @cls.to_s
      end

      def value
        get_inspect
      end

      # Data belonging to that appropriate class,
      # not to be used (use real accessors instead)
      attr_reader :rdata

      # Return the RR object in binary data format, suitable
      # for using in network streams.
      #
      #   raw_data = rr.data
      #   puts "RR is #{raw_data.size} bytes long"
      #
      def data
        str = pack_name(@name)
        str + [@type.to_i, @cls.to_i, ttl, @rdlength].pack("n2 N n") + get_data
      end

      # Return the RR object in binary data format, suitable
      # for using in network streams, with names compressed.
      # Must pass as arguments the offset inside the packet
      # and an hash of compressed names.
      #
      # This method is to be used in other classes and is
      # not intended for user space programs.
      #
      # TO FIX in one of the future releases
      #
      def comp_data(offset, compnames)
        str, offset, names = dn_comp(@name, offset, compnames)
        str    += [@type.to_i, @cls.to_i, ttl, @rdlength].pack("n2 N n")
        offset += Net::DNS::RRFIXEDSZ
        [str, offset, names]
      end

      # Returns a human readable representation of this record.
      # The value is always a String.
      #
      #   mx = Net::DNS::RR.new("example.com. 7200 MX 10 mailhost.example.com.")
      #   #=> example.com.            7200    IN      MX      10 mailhost.example.com.
      #
      def inspect
        to_s
      end

      # Returns a String representation of this record.
      #
      #   mx = Net::DNS::RR.new("example.com. 7200 MX 10 mailhost.example.com.")
      #   mx.to_s
      #   #=> "example.com.            7200    IN      MX      10 mailhost.example.com."
      #
      def to_s
        items = to_a.map(&:to_s)
        if @name.size < 24
          items.pack("A24 A8 A8 A8 A*")
        else
          items.join("   ")
        end.to_s
      end

      # Returns an Array with all the attributes for this record.
      #
      #   mx = Net::DNS::RR.new("example.com. 7200 MX 10 mailhost.example.com.")
      #   mx.to_a
      #   #=> ["example.com.", 7200, "IN", "MX", "10 mailhost.example.com."]
      #
      def to_a
        [name, ttl, cls.to_s, type.to_s, value]
      end

      private

      def new_from_string(rrstring)
        unless rrstring =~ RR_REGEXP
          raise ArgumentError,
                "Format error for RR string (maybe CLASS and TYPE not valid?)"
        end

        # Name of RR - mandatory
        begin
          @name = Regexp.last_match(1).downcase
        rescue NoMethodError
          raise ArgumentError, "Missing name field in RR string #{rrstring}"
        end

        # Time to live for RR, default 3 hours
        @ttl = Regexp.last_match(2) ? Regexp.last_match(2).to_i : 10_800

        # RR class, default to IN
        @cls = Net::DNS::RR::Classes.new Regexp.last_match(3)

        # RR type, default to A
        @type = Net::DNS::RR::Types.new Regexp.last_match(4)

        # All the rest is data
        @rdata = Regexp.last_match(5) ? Regexp.last_match(5).strip : ""

        if self.class == Net::DNS::RR
          Net::DNS::RR.const_get(@type.to_s).new(rrstring)
        else
          subclass_new_from_string(@rdata)
          self.class
        end
      end

      def new_from_hash(args)
        # Name field is mandatory
        unless args.key? :name
          raise ArgumentError, ":name field is mandatory"
        end

        @name  = args[:name].downcase
        @ttl   = args[:ttl] ? args[:ttl].to_i : 10_800 # Default 3 hours
        @type  = Net::DNS::RR::Types.new args[:type]
        @cls = Net::DNS::RR::Classes.new args[:cls]

        @rdata = args[:rdata] ? args[:rdata].strip : ""
        @rdlength = args[:rdlength] || @rdata.size

        if self.class == Net::DNS::RR
          Net::DNS::RR.const_get(@type.to_s).new(args)
        else
          hash = args.delete_if { |k, _| %i[name ttl type cls].include?(k) }
          if hash.key? :rdata
            subclass_new_from_string(hash[:rdata])
          else
            subclass_new_from_hash(hash)
          end
          self.class
        end
      end

      def new_from_binary(data, offset)
        if self.class == Net::DNS::RR
          temp = dn_expand(data, offset)[1]
          type = Net::DNS::RR::Types.new data.unpack("@#{temp} n")[0]
          (eval "Net::DNS::RR::#{type}").parse_packet(data, offset)
        else
          @name, offset = dn_expand(data, offset)
          rrtype, cls, @ttl, @rdlength = data.unpack("@#{offset} n2 N n")
          @type = Net::DNS::RR::Types.new rrtype
          @cls = Net::DNS::RR::Classes.new cls
          offset += RRFIXEDSZ
          offset = subclass_new_from_binary(data, offset)
          build_pack
          set_type
          [self, offset]
        end
      end

      # Methods to be overridden by subclasses
      def subclass_new_from_array(arr)
      end

      def subclass_new_from_string(str)
      end

      def subclass_new_from_hash(hash)
      end

      def subclass_new_from_binary(data, offset)
      end

      def build_pack
      end

      def get_inspect
        @rdata
      end

      def get_data
        @rdata
      end

      def set_type
        # TODO: Here we should probably
        # raise NotImplementedError
        # if we want the method to be implemented in any subclass.
      end

      def self.new(*args)
        o   = allocate
        obj = o.send(:initialize, *args)
        if self == Net::DNS::RR
          obj
        else
          o
        end
      end
    end
  end
end
