// Copyright 2017 Amagicom AB.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.

//! Bindings to [`SCDynamicStore`].
//!
//! See the examples directory for examples how to use this module.
//!
//! [`SCDynamicStore`]: https://developer.apple.com/documentation/systemconfiguration/scdynamicstore?language=objc

use crate::sys::{
    dynamic_store::{
        kSCDynamicStoreUseSessionKeys, SCDynamicStoreCallBack, SCDynamicStoreContext,
        SCDynamicStoreCopyKeyList, SCDynamicStoreCopyValue, SCDynamicStoreCreateRunLoopSource,
        SCDynamicStoreCreateWithOptions, SCDynamicStoreGetTypeID, SCDynamicStoreRef,
        SCDynamicStoreRemoveValue, SCDynamicStoreSetNotificationKeys, SCDynamicStoreSetValue,
    },
    dynamic_store_copy_specific::SCDynamicStoreCopyProxies,
};
use core_foundation::{
    array::{CFArray, CFArrayRef},
    base::{kCFAllocatorDefault, CFType, TCFType},
    boolean::CFBoolean,
    dictionary::CFDictionary,
    propertylist::{CFPropertyList, CFPropertyListSubClass},
    runloop::CFRunLoopSource,
    string::CFString,
};
use std::{ffi::c_void, ptr};

/// Struct describing the callback happening when a watched value in the dynamic store is changed.
pub struct SCDynamicStoreCallBackContext<T> {
    /// The callback function that will be called when a watched value in the dynamic store is
    /// changed.
    pub callout: SCDynamicStoreCallBackT<T>,

    /// The argument passed to each `callout` call. Can be used to keep state between
    /// callbacks.
    pub info: T,
}

/// Signature for callback functions getting called when a watched value in the dynamic store is
/// changed.
///
/// This is the safe callback definition, abstracting over the lower level `SCDynamicStoreCallBack`
/// from the `system-configuration-sys` crate.
pub type SCDynamicStoreCallBackT<T> =
    fn(store: SCDynamicStore, changed_keys: CFArray<CFString>, info: &mut T);

/// Builder for [`SCDynamicStore`] sessions.
///
/// [`SCDynamicStore`]: struct.SCDynamicStore.html
pub struct SCDynamicStoreBuilder<T> {
    name: CFString,
    session_keys: bool,
    callback_context: Option<SCDynamicStoreCallBackContext<T>>,
}

impl SCDynamicStoreBuilder<()> {
    /// Creates a new builder. `name` is used as the name parameter when creating the
    /// [`SCDynamicStore`] session.
    ///
    /// [`SCDynamicStore`]: struct.SCDynamicStore.html
    pub fn new<S: Into<CFString>>(name: S) -> Self {
        SCDynamicStoreBuilder {
            name: name.into(),
            session_keys: false,
            callback_context: None,
        }
    }
}

impl<T> SCDynamicStoreBuilder<T> {
    /// Set whether or not the created [`SCDynamicStore`] should have session keys or not.
    /// See [`SCDynamicStoreCreateWithOptions`] for details.
    ///
    /// Defaults to `false`.
    ///
    /// [`SCDynamicStore`]: struct.SCDynamicStore.html
    /// [`SCDynamicStoreCreateWithOptions`]: https://developer.apple.com/documentation/systemconfiguration/1437818-scdynamicstorecreatewithoptions?language=objc
    pub fn session_keys(mut self, session_keys: bool) -> Self {
        self.session_keys = session_keys;
        self
    }

    /// Set a callback context (callback function and data to pass to each callback call).
    ///
    /// Defaults to having callbacks disabled.
    pub fn callback_context<T2>(
        self,
        callback_context: SCDynamicStoreCallBackContext<T2>,
    ) -> SCDynamicStoreBuilder<T2> {
        SCDynamicStoreBuilder {
            name: self.name,
            session_keys: self.session_keys,
            callback_context: Some(callback_context),
        }
    }

    /// Create the dynamic store session.
    pub fn build(mut self) -> Option<SCDynamicStore> {
        let store_options = self.create_store_options();
        if let Some(callback_context) = self.callback_context.take() {
            SCDynamicStore::create(
                &self.name,
                &store_options,
                Some(convert_callback::<T>),
                &mut self.create_context(callback_context),
            )
        } else {
            SCDynamicStore::create(&self.name, &store_options, None, ptr::null_mut())
        }
    }

    fn create_store_options(&self) -> CFDictionary {
        let key = unsafe { CFString::wrap_under_create_rule(kSCDynamicStoreUseSessionKeys) };
        let value = CFBoolean::from(self.session_keys);
        let typed_dict = CFDictionary::from_CFType_pairs(&[(key, value)]);
        unsafe { CFDictionary::wrap_under_get_rule(typed_dict.as_concrete_TypeRef()) }
    }

    fn create_context(
        &self,
        callback_context: SCDynamicStoreCallBackContext<T>,
    ) -> SCDynamicStoreContext {
        // move the callback context struct to the heap and "forget" it.
        // It will later be brought back into the Rust typesystem and freed in
        // `release_callback_context`
        let info_ptr = Box::into_raw(Box::new(callback_context));

        SCDynamicStoreContext {
            version: 0,
            info: info_ptr as *mut _ as *mut c_void,
            retain: None,
            release: Some(release_callback_context::<T>),
            copyDescription: None,
        }
    }
}

declare_TCFType! {
    /// Access to the key-value pairs in the dynamic store of a running system.
    ///
    /// Use the [`SCDynamicStoreBuilder`] to create instances of this.
    ///
    /// [`SCDynamicStoreBuilder`]: struct.SCDynamicStoreBuilder.html
    SCDynamicStore, SCDynamicStoreRef
}

impl_TCFType!(SCDynamicStore, SCDynamicStoreRef, SCDynamicStoreGetTypeID);

impl SCDynamicStore {
    /// Creates a new session used to interact with the dynamic store maintained by the System
    /// Configuration server.
    fn create(
        name: &CFString,
        store_options: &CFDictionary,
        callout: SCDynamicStoreCallBack,
        context: *mut SCDynamicStoreContext,
    ) -> Option<Self> {
        unsafe {
            let store = SCDynamicStoreCreateWithOptions(
                kCFAllocatorDefault,
                name.as_concrete_TypeRef(),
                store_options.as_concrete_TypeRef(),
                callout,
                context,
            );
            if store.is_null() {
                None
            } else {
                Some(SCDynamicStore::wrap_under_create_rule(store))
            }
        }
    }

    /// Returns the keys that represent the current dynamic store entries that match the specified
    /// pattern. Or `None` if an error occurred.
    ///
    /// `pattern` - A regular expression pattern used to match the dynamic store keys.
    pub fn get_keys<S: Into<CFString>>(&self, pattern: S) -> Option<CFArray<CFString>> {
        let cf_pattern = pattern.into();
        unsafe {
            let array_ref = SCDynamicStoreCopyKeyList(
                self.as_concrete_TypeRef(),
                cf_pattern.as_concrete_TypeRef(),
            );
            if array_ref.is_null() {
                None
            } else {
                Some(CFArray::wrap_under_create_rule(array_ref))
            }
        }
    }

    /// Returns the key-value pairs that represent the current internet proxy settings. Or `None` if
    /// no proxy settings have been defined or if an error occurred.
    pub fn get_proxies(&self) -> Option<CFDictionary<CFString, CFType>> {
        unsafe {
            let dictionary_ref = SCDynamicStoreCopyProxies(self.as_concrete_TypeRef());
            if !dictionary_ref.is_null() {
                Some(CFDictionary::wrap_under_create_rule(dictionary_ref))
            } else {
                None
            }
        }
    }

    /// If the given key exists in the store, the associated value is returned.
    ///
    /// Use `CFPropertyList::downcast_into` to cast the result into the correct type.
    pub fn get<S: Into<CFString>>(&self, key: S) -> Option<CFPropertyList> {
        let cf_key = key.into();
        unsafe {
            let dict_ref =
                SCDynamicStoreCopyValue(self.as_concrete_TypeRef(), cf_key.as_concrete_TypeRef());
            if !dict_ref.is_null() {
                Some(CFPropertyList::wrap_under_create_rule(dict_ref))
            } else {
                None
            }
        }
    }

    /// Sets the value of the given key. Overwrites existing values.
    /// Returns `true` on success, false on failure.
    pub fn set<S: Into<CFString>, V: CFPropertyListSubClass>(&self, key: S, value: V) -> bool {
        self.set_raw(key, &value.into_CFPropertyList())
    }

    /// Sets the value of the given key. Overwrites existing values.
    /// Returns `true` on success, false on failure.
    pub fn set_raw<S: Into<CFString>>(&self, key: S, value: &CFPropertyList) -> bool {
        let cf_key = key.into();
        let success = unsafe {
            SCDynamicStoreSetValue(
                self.as_concrete_TypeRef(),
                cf_key.as_concrete_TypeRef(),
                value.as_concrete_TypeRef(),
            )
        };
        success != 0
    }

    /// Removes the value of the specified key from the dynamic store.
    pub fn remove<S: Into<CFString>>(&self, key: S) -> bool {
        let cf_key = key.into();
        let success = unsafe {
            SCDynamicStoreRemoveValue(self.as_concrete_TypeRef(), cf_key.as_concrete_TypeRef())
        };
        success != 0
    }

    /// Specifies a set of keys and key patterns that should be monitored for changes.
    pub fn set_notification_keys<T1, T2>(
        &self,
        keys: &CFArray<T1>,
        patterns: &CFArray<T2>,
    ) -> bool {
        let success = unsafe {
            SCDynamicStoreSetNotificationKeys(
                self.as_concrete_TypeRef(),
                keys.as_concrete_TypeRef(),
                patterns.as_concrete_TypeRef(),
            )
        };
        success != 0
    }

    /// Creates a run loop source object that can be added to the application's run loop.
    pub fn create_run_loop_source(&self) -> Option<CFRunLoopSource> {
        unsafe {
            let run_loop_source_ref = SCDynamicStoreCreateRunLoopSource(
                kCFAllocatorDefault,
                self.as_concrete_TypeRef(),
                0,
            );
            if run_loop_source_ref.is_null() {
                None
            } else {
                Some(CFRunLoopSource::wrap_under_create_rule(run_loop_source_ref))
            }
        }
    }
}

/// The raw callback used by the safe `SCDynamicStore` to convert from the `SCDynamicStoreCallBack`
/// to the `SCDynamicStoreCallBackT`
unsafe extern "C" fn convert_callback<T>(
    store_ref: SCDynamicStoreRef,
    changed_keys_ref: CFArrayRef,
    context_ptr: *mut c_void,
) {
    let store = SCDynamicStore::wrap_under_get_rule(store_ref);
    let changed_keys = CFArray::<CFString>::wrap_under_get_rule(changed_keys_ref);
    let context = &mut *(context_ptr as *mut _ as *mut SCDynamicStoreCallBackContext<T>);

    (context.callout)(store, changed_keys, &mut context.info);
}

// Release function called by core foundation on release of the dynamic store context.
unsafe extern "C" fn release_callback_context<T>(context_ptr: *const c_void) {
    // Bring back the context object from raw ptr so it is correctly freed.
    let _context = Box::from_raw(context_ptr as *mut SCDynamicStoreCallBackContext<T>);
}
