#![allow(missing_debug_implementations)]

mod config;

pub use config::*;

use crate::v4::message::*;
use mac_address::get_mac_address;
use rand::RngCore;
use rand::rngs::ThreadRng;
use std::net::Ipv4Addr;

/// The standard port for dhcpv4 clients defined in the RFC.
pub const CLIENT_PORT: u16 = 68;
/// This is an arbitrary port number above 1024 that can be used when testing the client.
/// Port numbers below 1024 are privileged ports that require admin access to bind to.
/// See: <https://www.w3.org/Daemon/User/Installation/PrivilegedPorts.html>
pub const TEST_CLIENT_PORT: u16 = 8068;
/// Resolves to TEST_PORT for debug builds and PORT for release builds
pub const RESOLVED_CLIENT_PORT: u16 = if cfg!(debug_assertions) {
    TEST_CLIENT_PORT
} else {
    CLIENT_PORT
};

fn get_chaddr() -> [u8; 16] {
    let address = get_mac_address().unwrap().unwrap().bytes();
    let mut chaddr: [u8; 16] = [0; 16];
    chaddr[..6].copy_from_slice(&address[..6]);
    chaddr
}

/// Logic for a DHCP client using this lib.
///
/// Used by the client binary and integration tests.
///
/// Requires the `v4_client` feature, which is enabled by default.
pub struct Client {
    /// A UdpSocket that understands how to use `Message` to communicate with the Server
    pub socket: Socket,
    /// The client's hardware address.
    ///
    /// This is cached for use in multiple messages to the server,
    /// and the server will reuse it in its replies.
    pub haddr: [u8; 16],
    /// The random number generator used to generate the number for xid.
    ///
    /// Defaults to `ThreadRng`, but can be set to a custom value
    /// with `Client.set_rng()`.
    rng: Box<dyn RngCore>,
    // TODO store current lease here
}

impl Client {
    /// A wrapper around [UdpSocket::bind](https://doc.rust-lang.org/std/net/struct.UdpSocket.html#method.bind)
    pub fn new(config: ClientConfig) -> Self {
        Self {
            socket: Socket::new(config.socket_config),
            haddr: get_chaddr(),
            rng: Box::new(ThreadRng::default()),
        }
    }

    /// Change the default random number generator from [ThreadRng](https://docs.rs/rand/latest/rand/rngs/struct.ThreadRng.html) to any rng that implements RngCore from the [rand](https://docs.rs/rand/latest/rand/rngs/index.html) crate.
    ///
    /// For example, you could use [SmallRng](https://docs.rs/rand/latest/rand/rngs/struct.SmallRng.html)
    /// which is sometimes faster, but has a higher collision rate of 1 in 65,000:
    /// ```
    /// use toe_beans::v4::{Client, ClientConfig};
    /// use std::net::{Ipv4Addr, SocketAddrV4};
    /// use rand::SeedableRng;
    /// use rand::rngs::SmallRng;
    ///
    /// let client_config = ClientConfig::default();
    /// let mut client = Client::new(client_config);
    ///
    /// // initialize the random number generator once.
    /// let rng = SmallRng::from_os_rng();
    ///
    /// client.set_rng(Box::new(rng));
    /// ```
    pub fn set_rng(&mut self, rng: Box<dyn RngCore>) {
        self.rng = rng;
    }

    /// Generates a number for xid by using the cached, initialized rng.
    /// Note that generating a number mutates the internal state of the rng.
    fn get_xid(&mut self) -> u32 {
        self.rng.next_u32()
    }

    /// Sent from the client to server as the start of a DHCP conversation.
    pub fn discover(&mut self) -> Message {
        Message {
            op: Ops::Request,
            htype: HTypes::Ethernet,
            hlen: 6,
            hops: 0,
            xid: self.get_xid(),
            secs: 0,
            flags: Flags { broadcast: false }, // TODO or true if required
            ciaddr: Ipv4Addr::UNSPECIFIED,
            yiaddr: Ipv4Addr::UNSPECIFIED,
            siaddr: Ipv4Addr::UNSPECIFIED,
            giaddr: Ipv4Addr::UNSPECIFIED,
            chaddr: self.haddr,
            sname: SName::EMPTY,
            file: File::EMPTY,
            magic: MAGIC,
            options: vec![
                MessageOptions::MessageType(MessageTypes::Discover),
                // TODO may set option 50 to request an ip address
            ]
            .into(),
        }
    }

    /// Sent from client to server (during RENEWING/REBINDING) when the client has a lease but wants to renew or rebind it.
    ///
    /// A renew will unicast, whereas a rebind will broadcast.
    /// Rebinding is intended for multiple DHCP servers that can coordinate consistenency of their leases
    pub fn extend(&mut self, address: Ipv4Addr) -> Message {
        Message {
            op: Ops::Request,
            htype: HTypes::Ethernet,
            hlen: 6,
            hops: 0,
            xid: self.get_xid(),
            secs: 0,
            flags: Flags { broadcast: false },
            ciaddr: address, // MUST be client's IP address
            yiaddr: Ipv4Addr::UNSPECIFIED,
            siaddr: Ipv4Addr::UNSPECIFIED,
            giaddr: Ipv4Addr::UNSPECIFIED,
            chaddr: self.haddr,
            sname: SName::EMPTY,
            file: File::EMPTY,
            magic: MAGIC,
            options: vec![
                MessageOptions::MessageType(MessageTypes::Request),
                // MUST NOT fill in requested ip address
                // MUST NOT fill in server identifier
            ]
            .into(),
        }
    }

    /// Broadcast from client to server (during INIT-REBOOT) to verify a previously allocated, cached configuration.
    // Server SHOULD send a DHCPNAK message to the client if the 'requested IP address'
    // is incorrect, or is on the wrong network
    pub fn verify(&mut self, address: Ipv4Addr) -> Message {
        Message {
            op: Ops::Request,
            htype: HTypes::Ethernet,
            hlen: 6,
            hops: 0,
            xid: self.get_xid(),
            secs: 0,
            flags: Flags { broadcast: false },
            ciaddr: Ipv4Addr::UNSPECIFIED, // MUST be zero
            yiaddr: Ipv4Addr::UNSPECIFIED,
            siaddr: Ipv4Addr::UNSPECIFIED,
            giaddr: Ipv4Addr::UNSPECIFIED,
            chaddr: self.haddr,
            sname: SName::EMPTY,
            file: File::EMPTY,
            magic: MAGIC,
            options: vec![
                MessageOptions::MessageType(MessageTypes::Request),
                MessageOptions::RequestedIp(AddressOption::new(address)), // MUST be filled in with previously assigned address
                                                                          // MUST NOT fill in server identifier
            ]
            .into(),
        }
    }

    /// Broadcast from the client to server (during SELECTING) following an Offer message.
    pub fn request(&self, message: Message) -> Message {
        Message {
            op: Ops::Request,
            htype: HTypes::Ethernet,
            hlen: 6,
            hops: 0,
            xid: message.xid,
            secs: 0,
            flags: Flags { broadcast: false }, // TODO or true if required
            ciaddr: Ipv4Addr::UNSPECIFIED,     // MUST be zero
            yiaddr: Ipv4Addr::UNSPECIFIED,
            siaddr: Ipv4Addr::UNSPECIFIED,
            giaddr: Ipv4Addr::UNSPECIFIED,
            chaddr: message.chaddr,
            sname: SName::EMPTY,
            file: File::EMPTY,
            magic: MAGIC,
            options: vec![
                MessageOptions::MessageType(MessageTypes::Request),
                MessageOptions::RequestedIp(AddressOption::new(message.yiaddr)), // MUST be filled in
                MessageOptions::ServerIdentifier(AddressOption::new(message.siaddr)), // MUST be filled in
            ]
            .into(),
        }
    }

    /// Sent from the client to server (following an Ack)
    /// if the IP address is already in use.
    pub fn decline(&self, message: Message) -> Message {
        Message {
            op: Ops::Request,
            htype: HTypes::Ethernet,
            hlen: 6,
            hops: 0,
            xid: message.xid,
            secs: 0,
            flags: Flags { broadcast: false },
            ciaddr: Ipv4Addr::UNSPECIFIED,
            yiaddr: Ipv4Addr::UNSPECIFIED,
            siaddr: Ipv4Addr::UNSPECIFIED,
            giaddr: Ipv4Addr::UNSPECIFIED,
            chaddr: self.haddr,
            sname: SName::EMPTY,
            file: File::EMPTY,
            magic: MAGIC,
            options: vec![
                MessageOptions::MessageType(MessageTypes::Decline),
                MessageOptions::RequestedIp(AddressOption::new(message.yiaddr)), // MUST
                MessageOptions::ServerIdentifier(AddressOption::new(message.siaddr)), // MUST
                                                                                 // Must not:
                                                                                 // - Requested IP address
                                                                                 // - IP address lease time
                                                                                 // - Vendor class identifier
                                                                                 // - Parameter request list
                                                                                 // - Maximum message size
                                                                                 // - Site-specific
                                                                                 // - All others (than: use file/sname, client identifier, and message)
            ]
            .into(),
        }
    }

    /// Sent from the client to server.
    pub fn release(&mut self) -> Message {
        Message {
            op: Ops::Request,
            htype: HTypes::Ethernet,
            hlen: 6,
            hops: 0,
            xid: self.get_xid(),
            secs: 0,
            flags: Flags { broadcast: false },
            ciaddr: Ipv4Addr::UNSPECIFIED, // TODO client's network address
            yiaddr: Ipv4Addr::UNSPECIFIED,
            siaddr: Ipv4Addr::UNSPECIFIED,
            giaddr: Ipv4Addr::UNSPECIFIED,
            chaddr: self.haddr,
            sname: SName::EMPTY,
            file: File::EMPTY,
            magic: MAGIC,
            options: vec![
                MessageOptions::MessageType(MessageTypes::Release),
                MessageOptions::ServerIdentifier(AddressOption::new(Ipv4Addr::UNSPECIFIED)), // MUST, TODO server's address
                                                                                             // Must not:
                                                                                             // - Requested IP address
                                                                                             // - IP address lease time
                                                                                             // - Vendor class identifier
                                                                                             // - Parameter request list
                                                                                             // - Maximum message size
                                                                                             // - Site-specific
                                                                                             // - All others (than: use file/sname, client identifier, and message)
            ]
            .into(),
        }
    }

    /// Sent from the client to server.
    pub fn inform(&mut self, parameter_list: Vec<u8>) -> Message {
        Message {
            op: Ops::Request,
            htype: HTypes::Ethernet,
            hlen: 6,
            hops: 0,
            xid: self.get_xid(),
            secs: 0,
            flags: Flags { broadcast: false },
            ciaddr: Ipv4Addr::UNSPECIFIED, // TODO client's network address
            yiaddr: Ipv4Addr::UNSPECIFIED,
            siaddr: Ipv4Addr::UNSPECIFIED,
            giaddr: Ipv4Addr::UNSPECIFIED,
            chaddr: self.haddr,
            sname: SName::EMPTY,
            file: File::EMPTY,
            magic: MAGIC,
            options: vec![
                MessageOptions::MessageType(MessageTypes::Inform),
                MessageOptions::RequestedOptions(parameter_list),
            ]
            .into(),
        }
    }
}
