use core_foundation::{
    array::CFArray,
    base::{CFType, TCFType, ToVoid},
    dictionary::CFDictionary,
    propertylist::CFPropertyList,
    runloop::{kCFRunLoopCommonModes, CFRunLoop},
    string::CFString,
};
use system_configuration::{
    dynamic_store::{SCDynamicStore, SCDynamicStoreBuilder, SCDynamicStoreCallBackContext},
    sys::schema_definitions::kSCPropNetDNSServerAddresses,
};

// This example will watch the dynamic store for changes to any DNS setting. As soon as a change
// is detected, it will be printed to stdout.

fn main() {
    let callback_context = SCDynamicStoreCallBackContext {
        callout: my_callback,
        info: Context { call_count: 0 },
    };

    let store = SCDynamicStoreBuilder::new("my-watch-dns-store")
        .callback_context(callback_context)
        .build()
        .expect("Unable to create DynamicStore");

    let watch_keys: CFArray<CFString> = CFArray::from_CFTypes(&[]);
    let watch_patterns =
        CFArray::from_CFTypes(&[CFString::from("(State|Setup):/Network/Service/.*/DNS")]);

    if store.set_notification_keys(&watch_keys, &watch_patterns) {
        println!("Registered for notifications");
    } else {
        panic!("Unable to register notifications");
    }

    let run_loop_source = store
        .create_run_loop_source()
        .expect("Unable to create run loop source");
    let run_loop = CFRunLoop::get_current();
    run_loop.add_source(&run_loop_source, unsafe { kCFRunLoopCommonModes });

    println!("Entering run loop");
    CFRunLoop::run_current();
}

/// This struct acts as a user provided context/payload to each notification callback.
/// Here one can store any type of data or state needed in the callback function.
#[derive(Debug)]
struct Context {
    call_count: u64,
}

#[allow(clippy::needless_pass_by_value)]
fn my_callback(store: SCDynamicStore, changed_keys: CFArray<CFString>, context: &mut Context) {
    context.call_count += 1;
    println!("Callback call count: {}", context.call_count);

    for key in changed_keys.iter() {
        if let Some(addresses) = get_dns(&store, key.clone()) {
            println!("{} changed DNS to {:?}", *key, addresses);
        } else {
            println!("{} removed DNS", *key);
        }
    }
}

fn get_dns(store: &SCDynamicStore, path: CFString) -> Option<Vec<String>> {
    let dns_settings = store
        .get(path)
        .and_then(CFPropertyList::downcast_into::<CFDictionary>)?;
    let address_array = dns_settings
        .find(unsafe { kSCPropNetDNSServerAddresses }.to_void())
        .map(|ptr| unsafe { CFType::wrap_under_get_rule(*ptr) })
        .and_then(CFType::downcast_into::<CFArray>)?;
    let mut result = Vec::with_capacity(address_array.len() as usize);
    for address_ptr in &address_array {
        let address =
            unsafe { CFType::wrap_under_get_rule(*address_ptr) }.downcast_into::<CFString>()?;
        result.push(address.to_string())
    }
    Some(result)
}
