/*
 * Copyright (c) 2003 Apple Computer, Inc. All rights reserved.
 *
 * @APPLE_LICENSE_HEADER_START@
 * 
 * The contents of this file constitute Original Code as defined in and
 * are subject to the Apple Public Source License Version 1.1 (the
 * "License").  You may not use this file except in compliance with the
 * License.  Please obtain a copy of the License at
 * http://www.apple.com/publicsource and read it before using this file.
 * 
 * This Original Code and all software distributed under the License are
 * distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, EITHER
 * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
 * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE OR NON-INFRINGEMENT.  Please see the
 * License for the specific language governing rights and limitations
 * under the License.
 * 
 * @APPLE_LICENSE_HEADER_END@
 */

#include <SystemConfiguration/SystemConfiguration.h>
#include <SystemConfiguration/SCValidation.h>
#include "IOPMKeys.h"
#include "IOPMLib.h"
#include "IOPMLibPrivate.h"

enum {
    kIOPMMaxScheduledEntries = 1000
};

static CFComparisonResult compare_dates(CFDictionaryRef a1, CFDictionaryRef a2, void *c)
{
    CFDateRef   d1, d2;
    a1 = isA_CFDictionary(a1);
    a2 = isA_CFDictionary(a2);
    if(!a1) return kCFCompareGreaterThan;
    else if(!a2) return kCFCompareLessThan;
    
    d1 = isA_CFDate(CFDictionaryGetValue(a1, CFSTR(kIOPMPowerEventTimeKey)));
    d2 = isA_CFDate(CFDictionaryGetValue(a2, CFSTR(kIOPMPowerEventTimeKey)));
    if(!d1) return kCFCompareGreaterThan;
    else if(!d2) return kCFCompareLessThan;
    
    return CFDateCompare(d1, d2, 0);
}

static CFAbsoluteTime roundOffDate(CFAbsoluteTime time)
{
    // round time down to the closest multiple of 30 seconds
    // CFAbsoluteTimes are encoded as doubles
    return (CFAbsoluteTime)nearbyint((time - fmod(time, (double)30.0)));
}

static CFDictionaryRef _IOPMCreatePowerOnDictionary(CFAbsoluteTime the_time, CFStringRef the_id, CFStringRef type)
{
    CFMutableDictionaryRef          d;
    CFDateRef	                    the_date;

    // make sure my_id is valid or NULL
    the_id = isA_CFString(the_id);        
    // round wakeup time to last 30 second increment
    the_time = roundOffDate(the_time);
    // package AbsoluteTime as a date for CFType purposes
    the_date = CFDateCreate(0, the_time);
    d = CFDictionaryCreateMutable(0, 3, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
    if(!d) return NULL;
    CFDictionaryAddValue(d, CFSTR(kIOPMPowerEventTimeKey), the_date);
    if(!the_id) the_id = CFSTR("");
    CFDictionaryAddValue(d, CFSTR(kIOPMPowerEventAppNameKey), the_id);
    CFDictionaryAddValue(d, CFSTR(kIOPMPowerEventTypeKey), type);
    CFRelease(the_date);
    return d;
}

static bool inputsValid(CFDateRef time_to_wake, CFStringRef my_id, CFStringRef type)
{
    // NULL is an acceptable input for my_id
    if(!isA_CFDate(time_to_wake)) return false;
    if(!isA_CFString(type)) return false;
    if(!(CFEqual(type, CFSTR(kIOPMAutoWake)) || 
        CFEqual(type, CFSTR(kIOPMAutoPowerOn)) ||
        CFEqual(type, CFSTR(kIOPMAutoWakeOrPowerOn)) ||
        CFEqual(type, CFSTR(kIOPMAutoSleep)) ||
        CFEqual(type, CFSTR(kIOPMAutoShutdown))))
    {
        return false;
    }

    return true;
}

static bool addEntryAndSetPrefs(SCPreferencesRef prefs, CFStringRef type, CFDictionaryRef package)
{
    CFArrayRef              arr = 0;
    CFMutableArrayRef       new_arr = 0;
    bool                    ret = false;
    
    arr = isA_CFArray(SCPreferencesGetValue(prefs, type));
    if(arr)
    {
        // There is already an array - add this entry to the end of it
        new_arr = CFArrayCreateMutableCopy(0, 0, arr);
        CFArrayAppendValue(new_arr, package);
        
        // and sort it by wakeup time! Maintain the array in sorted order!
        CFArraySortValues(new_arr, CFRangeMake(0, CFArrayGetCount(new_arr)), (CFComparatorFunction)compare_dates, 0);
    } else
    {
        // There is not already an array in the prefs file. Create one with this entry.
        new_arr = (CFMutableArrayRef)CFArrayCreate(0, (const void **)&package, 1, &kCFTypeArrayCallBacks);
    }
    
    // Write it out
    if(!new_arr) 
    {
        ret = false;
        goto exit;
    }
    
    if(!SCPreferencesSetValue(prefs, type, new_arr)) 
    {
        ret = false;
        goto exit;
    }

    ret = true;
    
exit:
    if(new_arr) CFRelease(new_arr);
    return ret;
}

// returns true if an entry was successfully removed
// false otherwise (entry wasn't found, or couldn't be removed)
static bool removeEntryAndSetPrefs(SCPreferencesRef prefs, CFStringRef type, CFDictionaryRef package)
{
    CFArrayRef                  arr = 0;
    CFMutableArrayRef           mut_arr = 0;
    CFIndex                     i;
    CFDictionaryRef             cancelee = 0;
    bool                        ret = false;

    // Grab the specific array from which we want to remove the entry... wakeup or poweron?
    // or both?
    arr = isA_CFArray(SCPreferencesGetValue(prefs, type));
    if(!arr) 
    {
        ret = true;
        goto exit;
    }

    i = CFArrayBSearchValues(arr, CFRangeMake(0, CFArrayGetCount(arr)), package,
                            (CFComparatorFunction)compare_dates, 0);
    
    // did it return an index within the array?                        
    if(0 <= i <= CFArrayGetCount(arr))
    {
        cancelee = CFArrayGetValueAtIndex(arr, i);
        // is the date at that index equal to the date to cancel?
        if(kCFCompareEqualTo == compare_dates(package, cancelee, 0))
        {
            // We have confirmation on the dates and types being equal. Check the id.
            // BUG: May have trouble if multiple apps have scheduled a wakeup at the same time.
            if(kCFCompareEqualTo == CFStringCompare(CFDictionaryGetValue(package, CFSTR(kIOPMPowerEventAppNameKey)), 
                                                    CFDictionaryGetValue(cancelee, CFSTR(kIOPMPowerEventAppNameKey)), 0))
            {
                // This is the one to cancel
                mut_arr = CFArrayCreateMutableCopy(0, 0, arr);
                CFArrayRemoveValueAtIndex(mut_arr, i);
                if(!SCPreferencesSetValue(prefs, type, mut_arr)) 
                { 
                    ret = false;
                    goto exit;
                }

                // success!
                ret = true;
            }
        }
    }
    
    exit:
    if(mut_arr) CFRelease(mut_arr);
    return ret;
}


extern IOReturn IOPMSchedulePowerEvent(CFDateRef time_to_wake, CFStringRef my_id, CFStringRef type)
{
    CFDictionaryRef         package = 0;
    SCPreferencesRef        prefs = 0;
    IOReturn                ret = false;
    CFArrayRef              tmp_wakeup_arr;
    int                     total_count = 0;
    CFAbsoluteTime          abs_time_to_wake;

    //  verify inputs
    if(!inputsValid(time_to_wake, my_id, type))
    {        
        ret = kIOReturnBadArgument;
        goto exit;
    }

    abs_time_to_wake = CFDateGetAbsoluteTime(time_to_wake);
    if(abs_time_to_wake < (CFAbsoluteTimeGetCurrent() + 30.0))
    {
        ret = kIOReturnNotReady;
        goto exit;
    }
    
    //  package the event in a CFDictionary
    package = _IOPMCreatePowerOnDictionary(abs_time_to_wake, my_id, type);

    // Open the prefs file and grab the current array
    prefs = SCPreferencesCreate(0, CFSTR("IOKit-AutoWake"), CFSTR(kIOPMAutoWakePrefsPath));
    if(!prefs || !SCPreferencesLock(prefs, true))
    {
        if(kSCStatusAccessError == SCError())
            ret = kIOReturnNotPrivileged;
        else ret = kIOReturnError;
        goto exit;
    }
    
    // Examine number of entries currently in disk and bail if too many
    total_count = 0;
    tmp_wakeup_arr = isA_CFArray(SCPreferencesGetValue(prefs, CFSTR(kIOPMAutoPowerOn)));
    if(tmp_wakeup_arr) total_count += CFArrayGetCount(tmp_wakeup_arr);
    tmp_wakeup_arr = isA_CFArray(SCPreferencesGetValue(prefs, CFSTR(kIOPMAutoWake)));
    if(tmp_wakeup_arr) total_count += CFArrayGetCount(tmp_wakeup_arr);
    tmp_wakeup_arr = isA_CFArray(SCPreferencesGetValue(prefs, CFSTR(kIOPMAutoSleep)));
    if(tmp_wakeup_arr) total_count += CFArrayGetCount(tmp_wakeup_arr);
    tmp_wakeup_arr = isA_CFArray(SCPreferencesGetValue(prefs, CFSTR(kIOPMAutoShutdown)));
    if(tmp_wakeup_arr) total_count += CFArrayGetCount(tmp_wakeup_arr);
    if(total_count >= kIOPMMaxScheduledEntries)
    {
        ret = kIOReturnNoSpace;
        goto exit;
    }

    // add the CFDictionary to the CFArray in SCPreferences file
    if(CFEqual(type, CFSTR(kIOPMAutoWakeOrPowerOn)))
    {
        // add to both lists    
        addEntryAndSetPrefs(prefs, CFSTR(kIOPMAutoWake), package);
        addEntryAndSetPrefs(prefs, CFSTR(kIOPMAutoPowerOn), package);
    } else {
        // just add the entry to the one (wake or power on)
        addEntryAndSetPrefs(prefs, type, package);
    }

    // Add a warning to the file
    SCPreferencesSetValue(prefs, CFSTR("WARNING"), CFSTR("Do not edit this file by hand - it must remain in sorted-by-date order."));

    //  commit the SCPreferences file out to disk
    if(!SCPreferencesCommitChanges(prefs))
    {
        ret = kIOReturnError;
        goto exit;
    }

    ret = kIOReturnSuccess;

    exit:
    if(package) CFRelease(package);
    if(prefs) SCPreferencesUnlock(prefs);
    if(prefs) CFRelease(prefs);
    return ret;
}

extern IOReturn IOPMCancelScheduledPowerEvent(CFDateRef time_to_wake, CFStringRef my_id, CFStringRef wake_or_restart)
{
    CFDictionaryRef         package = 0;
    SCPreferencesRef        prefs = 0;
    bool                    changed = false;
    IOReturn                ret = kIOReturnError;
        
    if(!inputsValid(time_to_wake, my_id, wake_or_restart)) 
    {
        ret = kIOReturnBadArgument;
        goto exit;
    }

    package = _IOPMCreatePowerOnDictionary(CFDateGetAbsoluteTime(time_to_wake), my_id, wake_or_restart);
    if(!package) goto exit;

    // Open the prefs file and grab the current array
    prefs = SCPreferencesCreate(0, CFSTR("IOKit-AutoWake"), CFSTR(kIOPMAutoWakePrefsPath));
    if(!prefs || !SCPreferencesLock(prefs, true)) 
    {
        if(kSCStatusAccessError == SCError())
            ret = kIOReturnNotPrivileged;
        else ret = kIOReturnError;
        goto exit;
    }
    
   // remove the CFDictionary from the CFArray in SCPreferences file
    if(CFEqual(wake_or_restart, CFSTR(kIOPMAutoWakeOrPowerOn)))
    {
        // add to both lists    
        changed = removeEntryAndSetPrefs(prefs, CFSTR(kIOPMAutoWake), package);
        changed |= removeEntryAndSetPrefs(prefs, CFSTR(kIOPMAutoPowerOn), package);
    } else {
        // just add the entry to the one (wake or power on or sleep or shutdown)
        changed = removeEntryAndSetPrefs(prefs, wake_or_restart, package);
    }
    
    if(changed)
    {
        // commit changes
        if(!SCPreferencesCommitChanges(prefs)) 
        {
            ret = kIOReturnError;
            goto exit;
        } else ret = kIOReturnSuccess;
    } else ret = kIOReturnNotFound;

exit:
    
    // release the lock and exit
    if(package) CFRelease(package);
    if(prefs) SCPreferencesUnlock(prefs);
    if(prefs) CFRelease(prefs);
    return ret;
}

extern CFArrayRef IOPMCopyScheduledPowerEvents
(void)
{
    SCPreferencesRef            prefs;
    CFArrayRef                  wake_arr, poweron_arr, sleep_arr, shutdown_arr;
    CFMutableArrayRef           new_arr;
    
    // Copy wakeup and restart arrays from SCPreferences
    prefs = SCPreferencesCreate(0, CFSTR("IOKit-AutoWake"), CFSTR(kIOPMAutoWakePrefsPath));
    if(!prefs) return NULL;

    wake_arr = isA_CFArray(SCPreferencesGetValue(prefs, CFSTR(kIOPMAutoWake)));
    poweron_arr = isA_CFArray(SCPreferencesGetValue(prefs, CFSTR(kIOPMAutoPowerOn)));
    sleep_arr = isA_CFArray(SCPreferencesGetValue(prefs, CFSTR(kIOPMAutoSleep)));
    shutdown_arr = isA_CFArray(SCPreferencesGetValue(prefs, CFSTR(kIOPMAutoShutdown)));
    
    new_arr = CFArrayCreateMutable(kCFAllocatorDefault, 0, &kCFTypeArrayCallBacks);
    if(wake_arr) CFArrayAppendArray(new_arr, wake_arr, CFRangeMake(0, CFArrayGetCount(wake_arr)));
    if(poweron_arr) CFArrayAppendArray(new_arr, poweron_arr, CFRangeMake(0, CFArrayGetCount(poweron_arr)));
    if(sleep_arr) CFArrayAppendArray(new_arr, sleep_arr, CFRangeMake(0, CFArrayGetCount(sleep_arr)));
    if(shutdown_arr) CFArrayAppendArray(new_arr, shutdown_arr, CFRangeMake(0, CFArrayGetCount(shutdown_arr)));

    CFRelease(prefs);
    
    // Return NULL if there are 0 entries in the array
    if(!new_arr) 
    {
        return NULL;
    } else {
        if(0 == CFArrayGetCount(new_arr))
        {
            CFRelease(new_arr);
            return NULL;
        }
    }    
    
    // Return the combined arrays
    return (CFArrayRef)new_arr;
}
