// Copyright 2015-2021 Benjamin Fry <benjaminfry@me.com>
//
// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
// https://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
// https://opensource.org/licenses/MIT>, at your option. This file may not be
// copied, modified, or distributed except according to those terms.

//! record type definitions
#![allow(clippy::use_self)]

use std::cmp::Ordering;
use std::fmt::{self, Display, Formatter};
use std::str::FromStr;

#[cfg(feature = "serde-config")]
use serde::{Deserialize, Serialize};

use crate::error::*;
use crate::serialize::binary::*;

// TODO: adopt proper restrictions on usage: https://tools.ietf.org/html/rfc6895 section 3.1
//  add the data TYPEs, QTYPEs, and Meta-TYPEs
//

/// The type of the resource record.
///
/// This specifies the type of data in the RData field of the Resource Record
#[cfg_attr(feature = "serde-config", derive(Deserialize, Serialize))]
#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone)]
#[allow(dead_code)]
#[non_exhaustive]
pub enum RecordType {
    /// [RFC 1035](https://tools.ietf.org/html/rfc1035) IPv4 Address record
    A,
    /// [RFC 3596](https://tools.ietf.org/html/rfc3596) IPv6 address record
    AAAA,
    /// [ANAME draft-ietf-dnsop-aname](https://tools.ietf.org/html/draft-ietf-dnsop-aname-04)
    ANAME,
    //  AFSDB,      //	18	RFC 1183	AFS database record
    /// [RFC 1035](https://tools.ietf.org/html/rfc1035) All cached records, aka ANY
    ANY,
    //  APL,        //	42	RFC 3123	Address Prefix List
    /// [RFC 1035](https://tools.ietf.org/html/rfc1035) Authoritative Zone Transfer
    AXFR,
    /// [RFC 6844](https://tools.ietf.org/html/rfc6844) Certification Authority Authorization
    CAA,
    /// [RFC 7344](https://tools.ietf.org/html/rfc7344) Child DS
    CDS,
    /// [RFC 7344](https://tools.ietf.org/html/rfc7344) Child DNSKEY
    CDNSKEY,
    //  CERT,       // 37 RFC 4398 Certificate record
    /// [RFC 1035](https://tools.ietf.org/html/rfc1035) Canonical name record
    CNAME,
    //  DHCID,      // 49 RFC 4701 DHCP identifier
    //  DLV,        //	32769	RFC 4431	DNSSEC Lookaside Validation record
    //  DNAME,      // 39 RFC 2672 Delegation Name
    /// [RFC 7477](https://tools.ietf.org/html/rfc4034) Child-to-parent synchronization record
    CSYNC,
    /// [RFC 4034](https://tools.ietf.org/html/rfc4034) DNS Key record: RSASHA256 and RSASHA512, RFC5702
    DNSKEY,
    /// [RFC 4034](https://tools.ietf.org/html/rfc4034) Delegation signer: RSASHA256 and RSASHA512, RFC5702
    DS,
    /// [RFC 1035](https://tools.ietf.org/html/rfc1035) host information
    HINFO,
    //  HIP,        // 55 RFC 5205 Host Identity Protocol
    /// [RFC draft-ietf-dnsop-svcb-https-03](https://tools.ietf.org/html/draft-ietf-dnsop-svcb-httpssvc-03) DNS SVCB and HTTPS RRs
    HTTPS,
    //  IPSECKEY,   // 45 RFC 4025 IPsec Key
    /// [RFC 1996](https://tools.ietf.org/html/rfc1996) Incremental Zone Transfer
    IXFR,
    //  KX,         // 36 RFC 2230 Key eXchanger record
    /// [RFC 2535](https://tools.ietf.org/html/rfc2535) and [RFC 2930](https://tools.ietf.org/html/rfc2930) Key record
    KEY,
    //  LOC,        // 29 RFC 1876 Location record
    /// [RFC 1035](https://tools.ietf.org/html/rfc1035) Mail exchange record
    MX,
    /// [RFC 3403](https://tools.ietf.org/html/rfc3403) Naming Authority Pointer
    NAPTR,
    /// [RFC 1035](https://tools.ietf.org/html/rfc1035) Name server record
    NS,
    /// [RFC 4034](https://tools.ietf.org/html/rfc4034) Next-Secure record
    NSEC,
    /// [RFC 5155](https://tools.ietf.org/html/rfc5155) NSEC record version 3
    NSEC3,
    /// [RFC 5155](https://tools.ietf.org/html/rfc5155) NSEC3 parameters
    NSEC3PARAM,
    /// [RFC 1035](https://tools.ietf.org/html/rfc1035) Null server record, for testing
    NULL,
    /// [RFC 7929](https://tools.ietf.org/html/rfc7929) OpenPGP public key
    OPENPGPKEY,
    /// [RFC 6891](https://tools.ietf.org/html/rfc6891) Option
    OPT,
    /// [RFC 1035](https://tools.ietf.org/html/rfc1035) Pointer record
    PTR,
    //  RP,         // 17 RFC 1183 Responsible person
    /// [RFC 4034](https://tools.ietf.org/html/rfc4034) DNSSEC signature: RSASHA256 and RSASHA512, RFC5702
    RRSIG,
    /// [RFC 2535](https://tools.ietf.org/html/rfc2535) (and [RFC 2931](https://tools.ietf.org/html/rfc2931)) Signature, to support [RFC 2137](https://tools.ietf.org/html/rfc2137) Update.
    SIG,
    /// [RFC 1035](https://tools.ietf.org/html/rfc1035) and [RFC 2308](https://tools.ietf.org/html/rfc2308) Start of [a zone of] authority record
    SOA,
    /// [RFC 2782](https://tools.ietf.org/html/rfc2782) Service locator
    SRV,
    /// [RFC 4255](https://tools.ietf.org/html/rfc4255) SSH Public Key Fingerprint
    SSHFP,
    /// [RFC draft-ietf-dnsop-svcb-https-03](https://tools.ietf.org/html/draft-ietf-dnsop-svcb-httpssvc-03) DNS SVCB and HTTPS RRs
    SVCB,
    //  TA,         // 32768 N/A DNSSEC Trust Authorities
    //  TKEY,       // 249 RFC 2930 Secret key record
    /// [RFC 6698](https://tools.ietf.org/html/rfc6698) TLSA certificate association
    TLSA,
    /// [RFC 8945](https://tools.ietf.org/html/rfc8945) Transaction Signature
    TSIG,
    /// [RFC 1035](https://tools.ietf.org/html/rfc1035) Text record
    TXT,
    /// Unknown Record type, or unsupported
    Unknown(u16),

    /// This corresponds to a record type of 0, unspecified
    ZERO,
}

impl RecordType {
    /// Returns true if this is an ANY
    #[inline]
    pub fn is_any(self) -> bool {
        self == Self::ANY
    }

    /// Returns true if this is a CNAME
    #[inline]
    pub fn is_cname(self) -> bool {
        self == Self::CNAME
    }

    /// Returns true if this is an NS
    #[inline]
    pub fn is_ns(self) -> bool {
        self == Self::NS
    }

    /// Returns true if this is an SOA
    #[inline]
    pub fn is_soa(self) -> bool {
        self == Self::SOA
    }

    /// Returns true if this is an SRV
    #[inline]
    pub fn is_srv(self) -> bool {
        self == Self::SRV
    }

    /// Returns true if this is an A or an AAAA record
    #[inline]
    pub fn is_ip_addr(self) -> bool {
        matches!(self, Self::A | Self::AAAA)
    }

    /// Returns true if this is a DNSSEC RecordType
    #[inline]
    pub fn is_dnssec(self) -> bool {
        matches!(
            self,
            Self::DNSKEY
                | Self::CDNSKEY
                | Self::CDS
                | Self::DS
                | Self::KEY
                | Self::NSEC
                | Self::NSEC3
                | Self::NSEC3PARAM
                | Self::RRSIG
                | Self::SIG
                | Self::TSIG
        )
    }

    /// Returns true if this is a Zero (unspecified) RecordType
    #[inline]
    pub fn is_zero(self) -> bool {
        self == Self::ZERO
    }
}

impl FromStr for RecordType {
    type Err = ProtoError;

    /// Convert `&str` to `RecordType`
    ///
    /// ```
    /// use std::str::FromStr;
    /// use hickory_proto::rr::record_type::RecordType;
    ///
    /// let var: RecordType = RecordType::from_str("A").unwrap();
    /// assert_eq!(RecordType::A, var);
    /// ```
    fn from_str(str: &str) -> ProtoResult<Self> {
        // TODO missing stuff?
        debug_assert!(str.chars().all(|x| !char::is_ascii_lowercase(&x)));
        match str {
            "A" => Ok(Self::A),
            "AAAA" => Ok(Self::AAAA),
            "ANAME" => Ok(Self::ANAME),
            "AXFR" => Ok(Self::AXFR),
            "CAA" => Ok(Self::CAA),
            "CDNSKEY" => Ok(Self::CDNSKEY),
            "CDS" => Ok(Self::CDS),
            "CNAME" => Ok(Self::CNAME),
            "CSYNC" => Ok(Self::CSYNC),
            "DNSKEY" => Ok(Self::DNSKEY),
            "DS" => Ok(Self::DS),
            "HINFO" => Ok(Self::HINFO),
            "HTTPS" => Ok(Self::HTTPS),
            "KEY" => Ok(Self::KEY),
            "MX" => Ok(Self::MX),
            "NAPTR" => Ok(Self::NAPTR),
            "NSEC" => Ok(Self::NSEC),
            "NSEC3" => Ok(Self::NSEC3),
            "NSEC3PARAM" => Ok(Self::NSEC3PARAM),
            "NS" => Ok(Self::NS),
            "NULL" => Ok(Self::NULL),
            "OPENPGPKEY" => Ok(Self::OPENPGPKEY),
            "PTR" => Ok(Self::PTR),
            "RRSIG" => Ok(Self::RRSIG),
            "SIG" => Ok(Self::SIG),
            "SOA" => Ok(Self::SOA),
            "SRV" => Ok(Self::SRV),
            "SSHFP" => Ok(Self::SSHFP),
            "SVCB" => Ok(Self::SVCB),
            "TLSA" => Ok(Self::TLSA),
            "TXT" => Ok(Self::TXT),
            "TSIG" => Ok(Self::TSIG),
            "ANY" | "*" => Ok(Self::ANY),
            _ => Err(ProtoErrorKind::UnknownRecordTypeStr(str.to_string()).into()),
        }
    }
}

impl From<u16> for RecordType {
    /// Convert from `u16` to `RecordType`
    ///
    /// ```
    /// use hickory_proto::rr::record_type::RecordType;
    ///
    /// let var = RecordType::from(1);
    /// assert_eq!(RecordType::A, var);
    /// ```
    fn from(value: u16) -> Self {
        match value {
            1 => Self::A,
            28 => Self::AAAA,
            // TODO: wrong value here, see https://github.com/hickory-dns/hickory-dns/issues/723
            65305 => Self::ANAME,
            255 => Self::ANY,
            251 => Self::IXFR,
            252 => Self::AXFR,
            257 => Self::CAA,
            59 => Self::CDS,
            60 => Self::CDNSKEY,
            5 => Self::CNAME,
            62 => Self::CSYNC,
            48 => Self::DNSKEY,
            43 => Self::DS,
            13 => Self::HINFO,
            65 => Self::HTTPS,
            25 => Self::KEY,
            15 => Self::MX,
            35 => Self::NAPTR,
            2 => Self::NS,
            47 => Self::NSEC,
            50 => Self::NSEC3,
            51 => Self::NSEC3PARAM,
            10 => Self::NULL,
            61 => Self::OPENPGPKEY,
            41 => Self::OPT,
            12 => Self::PTR,
            46 => Self::RRSIG,
            24 => Self::SIG,
            6 => Self::SOA,
            33 => Self::SRV,
            44 => Self::SSHFP,
            64 => Self::SVCB,
            52 => Self::TLSA,
            250 => Self::TSIG,
            16 => Self::TXT,
            0 => Self::ZERO,
            // all unknown record types
            _ => Self::Unknown(value),
        }
    }
}

impl BinEncodable for RecordType {
    fn emit(&self, encoder: &mut BinEncoder<'_>) -> ProtoResult<()> {
        encoder.emit_u16((*self).into())
    }
}

impl<'r> BinDecodable<'r> for RecordType {
    fn read(decoder: &mut BinDecoder<'_>) -> ProtoResult<Self> {
        Ok(decoder
            .read_u16()
            .map(
                Restrict::unverified, /*RecordType is safe with any u16*/
            )
            .map(Self::from)?)
    }
}

// TODO make these a macro...

/// Convert from `RecordType` to `&str`
///
/// ```
/// use hickory_proto::rr::record_type::RecordType;
///
/// let var: &'static str = From::from(RecordType::A);
/// assert_eq!("A", var);
///
/// let var: &'static str = RecordType::A.into();
/// assert_eq!("A", var);
/// ```
impl From<RecordType> for &'static str {
    fn from(rt: RecordType) -> &'static str {
        match rt {
            RecordType::A => "A",
            RecordType::AAAA => "AAAA",
            RecordType::ANAME => "ANAME",
            RecordType::ANY => "ANY",
            RecordType::AXFR => "AXFR",
            RecordType::CAA => "CAA",
            RecordType::CDNSKEY => "CDNSKEY",
            RecordType::CDS => "CDS",
            RecordType::CNAME => "CNAME",
            RecordType::CSYNC => "CSYNC",
            RecordType::DNSKEY => "DNSKEY",
            RecordType::DS => "DS",
            RecordType::HINFO => "HINFO",
            RecordType::HTTPS => "HTTPS",
            RecordType::KEY => "KEY",
            RecordType::IXFR => "IXFR",
            RecordType::MX => "MX",
            RecordType::NAPTR => "NAPTR",
            RecordType::NS => "NS",
            RecordType::NSEC => "NSEC",
            RecordType::NSEC3 => "NSEC3",
            RecordType::NSEC3PARAM => "NSEC3PARAM",
            RecordType::NULL => "NULL",
            RecordType::OPENPGPKEY => "OPENPGPKEY",
            RecordType::OPT => "OPT",
            RecordType::PTR => "PTR",
            RecordType::RRSIG => "RRSIG",
            RecordType::SIG => "SIG",
            RecordType::SOA => "SOA",
            RecordType::SRV => "SRV",
            RecordType::SSHFP => "SSHFP",
            RecordType::SVCB => "SVCB",
            RecordType::TLSA => "TLSA",
            RecordType::TSIG => "TSIG",
            RecordType::TXT => "TXT",
            RecordType::ZERO => "ZERO",
            RecordType::Unknown(_) => "Unknown",
        }
    }
}

/// Convert from `RecordType` to `u16`
///
/// ```
/// use hickory_proto::rr::record_type::RecordType;
///
/// let var: u16 = RecordType::A.into();
/// assert_eq!(1, var);
/// ```
impl From<RecordType> for u16 {
    fn from(rt: RecordType) -> Self {
        match rt {
            RecordType::A => 1,
            RecordType::AAAA => 28,
            // TODO: wrong value here, see https://github.com/hickory-dns/hickory-dns/issues/723
            RecordType::ANAME => 65305,
            RecordType::ANY => 255,
            RecordType::AXFR => 252,
            RecordType::CAA => 257,
            RecordType::CDNSKEY => 60,
            RecordType::CDS => 59,
            RecordType::CNAME => 5,
            RecordType::CSYNC => 62,
            RecordType::DNSKEY => 48,
            RecordType::DS => 43,
            RecordType::HINFO => 13,
            RecordType::HTTPS => 65,
            RecordType::KEY => 25,
            RecordType::IXFR => 251,
            RecordType::MX => 15,
            RecordType::NAPTR => 35,
            RecordType::NS => 2,
            RecordType::NSEC => 47,
            RecordType::NSEC3 => 50,
            RecordType::NSEC3PARAM => 51,
            RecordType::NULL => 10,
            RecordType::OPENPGPKEY => 61,
            RecordType::OPT => 41,
            RecordType::PTR => 12,
            RecordType::RRSIG => 46,
            RecordType::SIG => 24,
            RecordType::SOA => 6,
            RecordType::SRV => 33,
            RecordType::SSHFP => 44,
            RecordType::SVCB => 64,
            RecordType::TLSA => 52,
            RecordType::TSIG => 250,
            RecordType::TXT => 16,
            RecordType::ZERO => 0,
            RecordType::Unknown(code) => code,
        }
    }
}

/// [Canonical DNS Name Order](https://tools.ietf.org/html/rfc4034#section-6)
impl PartialOrd<Self> for RecordType {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

/// [Canonical DNS Name Order](https://tools.ietf.org/html/rfc4034#section-6)
impl Ord for RecordType {
    fn cmp(&self, other: &Self) -> Ordering {
        u16::from(*self).cmp(&u16::from(*other))
    }
}

impl Display for RecordType {
    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> {
        f.write_str(Into::<&str>::into(*self))
    }
}

#[cfg(test)]
mod tests {
    #![allow(clippy::dbg_macro, clippy::print_stdout)]

    use super::*;

    #[test]
    fn test_order() {
        let ordered = vec![
            RecordType::A,
            RecordType::NS,
            RecordType::CNAME,
            RecordType::SOA,
            RecordType::NULL,
            RecordType::PTR,
            RecordType::HINFO,
            RecordType::MX,
            RecordType::TXT,
            RecordType::AAAA,
            RecordType::SRV,
            RecordType::CSYNC,
            RecordType::AXFR,
            RecordType::ANY,
        ];

        let mut unordered = vec![
            RecordType::ANY,
            RecordType::NULL,
            RecordType::AXFR,
            RecordType::A,
            RecordType::NS,
            RecordType::SOA,
            RecordType::SRV,
            RecordType::PTR,
            RecordType::MX,
            RecordType::CNAME,
            RecordType::TXT,
            RecordType::AAAA,
            RecordType::HINFO,
            RecordType::CSYNC,
        ];

        unordered.sort();

        for rtype in unordered.clone() {
            println!("u16 for {:?}: {}", rtype, u16::from(rtype));
        }

        assert_eq!(ordered, unordered);
    }

    /// Check that all record type names parse into unique `RecordType` instances,
    /// and can be converted back into the same name.
    #[test]
    fn test_record_type_parse() {
        let record_names = &[
            "A",
            "AAAA",
            "ANAME",
            "CAA",
            "CNAME",
            "CSYNC",
            "HINFO",
            "NULL",
            "MX",
            "NAPTR",
            "NS",
            "OPENPGPKEY",
            "PTR",
            "SOA",
            "SRV",
            "SSHFP",
            "TLSA",
            "TXT",
            "ANY",
            "AXFR",
        ];

        #[cfg(feature = "dnssec")]
        let dnssec_record_names = &[
            "CDNSKEY",
            "CDS",
            "DNSKEY",
            "DS",
            "KEY",
            "NSEC",
            "NSEC3",
            "NSEC3PARAM",
            "RRSIG",
            "SIG",
            "TSIG",
        ];
        #[cfg(not(feature = "dnssec"))]
        let dnssec_record_names = &[];

        let mut rtypes = std::collections::HashSet::new();
        for name in record_names.iter().chain(dnssec_record_names) {
            let rtype: RecordType = name.parse().unwrap();
            assert_eq!(rtype.to_string().to_ascii_uppercase().as_str(), *name);
            assert!(rtypes.insert(rtype));
        }
    }

    #[test]
    fn check_record_type_parse_wont_panic_with_symbols() {
        let dns_class = "a-b-c".to_ascii_uppercase().parse::<RecordType>();
        assert!(matches!(&dns_class, Err(ProtoError { .. })));
    }
}
