/*
 * Copyright (c) 2015 Apple Inc. All Rights Reserved.
 *
 * @APPLE_LICENSE_HEADER_START@
 *
 * This file contains Original Code and/or Modifications of Original Code
 * as defined in and that are subject to the Apple Public Source License
 * Version 2.0 (the 'License'). You may not use this file except in
 * compliance with the License. Please obtain a copy of the License at
 * http://www.opensource.apple.com/apsl/ and read it before using this
 * file.
 *
 * The 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, QUIET ENJOYMENT OR NON-INFRINGEMENT.
 * Please see the License for the specific language governing rights and
 * limitations under the License.
 *
 * @APPLE_LICENSE_HEADER_END@
 */

/*
 * SOSChangeTracker.c -  Implementation of a manifest caching change tracker that forwards changes to children
 */

#import "keychain/SecureObjectSync/SOSChangeTracker.h"
#include "keychain/SecureObjectSync/SOSDigestVector.h"
#include "keychain/SecureObjectSync/SOSEnginePriv.h"
#include "keychain/SecureObjectSync/SOSManifest.h"
#include "keychain/SecureObjectSync/SOSInternal.h"
#include <utilities/SecCFError.h>
#include <utilities/SecCFWrappers.h>

CFStringRef SOSChangeCopyDescription(SOSChangeRef change) {
    CFTypeRef object = NULL;
    bool isAdd = SOSChangeGetObject(change, &object);
    // TODO: Print objects or digests
    return (isData(object)
            ? isAdd ? CFSTR("a") : CFSTR("d")
            : isAdd ? CFSTR("A") : CFSTR("D"));
}

CFDataRef SOSChangeCopyDigest(SOSDataSourceRef dataSource, SOSChangeRef change, bool *isDel, SOSObjectRef *object, CFErrorRef *error) {
    CFDataRef digest = NULL;
    if (isArray(change)) {
        if (CFArrayGetCount(change) != 1) {
            SecError(errSecDecode, error, CFSTR("change array count: %ld"), CFArrayGetCount(change));
            return NULL;
        }
        change = CFArrayGetValueAtIndex(change, 0);
        *isDel = true;
    } else {
        *isDel = false;
    }

    // If the change is a CFData, this is the signal that it is a delete
    if (isData(change)) {
        digest = (CFDataRef)CFRetain(change);
    } else {
        digest = SOSObjectCopyDigest(dataSource, (SOSObjectRef)change, error);
        *object = (SOSObjectRef)change;
    }
    assert(digest && CFDataGetLength(digest) == CCSHA1_OUTPUT_SIZE);
    return digest;
}

CFStringRef SOSChangesCopyDescription(CFArrayRef changes) {
    CFMutableStringRef desc = CFStringCreateMutableCopy(kCFAllocatorDefault, 0, CFSTR("("));
    CFTypeRef change;
    if (changes) CFArrayForEachC(changes, change) {
        CFStringRef changeDesc = SOSChangeCopyDescription(change);
        CFStringAppend(desc, changeDesc);
        CFReleaseNull(changeDesc);
    }
    CFStringAppend(desc, CFSTR(")"));
    return desc;
}


/* SOSChangeTracker implementation. */
struct __OpaqueSOSChangeTracker {
    CFRuntimeBase _base;
    SOSManifestRef manifest;                // Optional: Only concrete cts have a manifest
    CFMutableArrayRef changeChildren;       // Optional: cts can have children
    CFMutableArrayRef manifestChildren;     // Optional: cts can have children
};

static CFStringRef SOSChangeTrackerCopyFormatDescription(CFTypeRef cf, CFDictionaryRef formatOptions) {
    SOSChangeTrackerRef ct = (SOSChangeTrackerRef)cf;
    CFStringRef desc = CFStringCreateWithFormat(kCFAllocatorDefault, formatOptions, CFSTR("<ChangeTracker %@ children %ld/%ld>"),
                                                ct->manifest ? ct->manifest : (SOSManifestRef)CFSTR("NonConcrete"),
                                                CFArrayGetCount(ct->changeChildren), CFArrayGetCount(ct->manifestChildren));
    return desc;
}

static void SOSChangeTrackerDestroy(CFTypeRef cf) {
    SOSChangeTrackerRef ct = (SOSChangeTrackerRef)cf;
    CFReleaseSafe(ct->manifest);
    CFReleaseSafe(ct->changeChildren);
    CFReleaseSafe(ct->manifestChildren);
}

// Even though SOSChangeTracker instances are used as keys in dictionaries, they are treated as pointers when used as such
// which is fine since the engine ensures instances are singletons.
CFGiblisFor(SOSChangeTracker);

SOSChangeTrackerRef SOSChangeTrackerCreate(CFAllocatorRef allocator, bool isConcrete, CFArrayRef changeChildren, CFErrorRef *error) {
    SOSChangeTrackerRef ct = NULL;
    ct = CFTypeAllocate(SOSChangeTracker, struct __OpaqueSOSChangeTracker, allocator);
    if (ct && isConcrete) {
        ct->manifest = SOSManifestCreateWithData(NULL, error);
        if (!ct->manifest)
            CFReleaseNull(ct);
    }
    if (ct) {
        if (changeChildren)
            ct->changeChildren = CFArrayCreateMutableCopy(kCFAllocatorDefault, 0, changeChildren);
        else
            ct->changeChildren = CFArrayCreateMutableForCFTypes(kCFAllocatorDefault);
        ct->manifestChildren = CFArrayCreateMutableForCFTypes(kCFAllocatorDefault);
    }

    return ct;
}

// Change the concreteness of the current ct (a non concrete ct does not support SOSChangeTrackerCopyManifest().
void SOSChangeTrackerSetConcrete(SOSChangeTrackerRef ct, bool isConcrete) {
    if (!isConcrete)
        CFReleaseNull(ct->manifest);
    else if (!ct->manifest) {
        ct->manifest = SOSManifestCreateWithData(NULL, NULL);
    }
}

// Add a child to the current ct
void SOSChangeTrackerRegisterChangeUpdate(SOSChangeTrackerRef ct, SOSChangeTrackerUpdatesChanges child) {
    CFArrayAppendValue(ct->changeChildren, child);
}

void SOSChangeTrackerRegisterManifestUpdate(SOSChangeTrackerRef ct, SOSChangeTrackerUpdatesManifests child) {
    CFArrayAppendValue(ct->manifestChildren, child);
}

void SOSChangeTrackerResetRegistration(SOSChangeTrackerRef ct) {
    CFArrayRemoveAllValues(ct->changeChildren);
    CFArrayRemoveAllValues(ct->manifestChildren);
}

void SOSChangeTrackerSetManifest(SOSChangeTrackerRef ct, SOSManifestRef manifest) {
    CFRetainAssign(ct->manifest, manifest);
}

SOSManifestRef SOSChangeTrackerCopyManifest(SOSChangeTrackerRef ct, CFErrorRef *error) {
    if (ct->manifest) {
        return (SOSManifestRef)CFRetain(ct->manifest);
    }
    SOSErrorCreate(kSOSErrorNotConcreteError, error, NULL, CFSTR("ChangeTracker is not concrete"));
    return NULL;
}

static bool SOSChangeTrackerCreateManifestsWithChanges(SOSEngineRef engine, CFArrayRef changes, SOSManifestRef *removals, SOSManifestRef *additions, CFErrorRef *error) {
    bool ok = true;
    struct SOSDigestVector dvdels = SOSDigestVectorInit;
    struct SOSDigestVector dvadds = SOSDigestVectorInit;
    struct SOSDigestVector *dv;
    CFTypeRef change;
    CFArrayForEachC(changes, change) {
        CFDataRef digest, allocatedDigest = NULL;
        if (isArray(change)) {
            assert(CFArrayGetCount(change) == 1);
            change = CFArrayGetValueAtIndex(change, 0);
            dv = &dvdels;
        } else {
            dv = &dvadds;
        }

        if (isData(change)) {
            digest = (CFDataRef)change;
        } else {
            CFErrorRef digestError = NULL;
            digest = allocatedDigest = SOSObjectCopyDigest(SOSEngineGetDataSource(engine), (SOSObjectRef)change, &digestError);
            if (!digest) {
                secerror("change %@ SOSObjectCopyDigest: %@", change, digestError);
                CFReleaseNull(digestError);
                continue;
            }
        }

        if (CFDataGetLength(digest) == 20) {
            SOSDigestVectorAppend(dv, CFDataGetBytePtr(digest));
        } else {
            secerror("change %@ bad length digest: %@", change, digest);
        }
        CFReleaseNull(allocatedDigest);
    }
    if (ok && removals)
        ok = *removals = SOSManifestCreateWithDigestVector(&dvdels, error);
    if (ok && additions)
        ok = *additions = SOSManifestCreateWithDigestVector(&dvadds, error);

    SOSDigestVectorFree(&dvadds);
    SOSDigestVectorFree(&dvdels);

    return ok;
}

bool SOSChangeTrackerTrackChanges(SOSChangeTrackerRef ct, SOSEngineRef engine, SOSTransactionRef txn, SOSDataSourceTransactionSource source, SOSDataSourceTransactionPhase phase, CFArrayRef changes, CFErrorRef *error) {
    bool ok = true;
    if (changes && CFArrayGetCount(changes)) {
        CFStringRef changesDesc = SOSChangesCopyDescription(changes);
        secnotice("tracker", "%@ %s %s changes: %@", ct, phase == kSOSDataSourceTransactionWillCommit ? "will-commit" : phase == kSOSDataSourceTransactionDidCommit ? "did-commit" : "did-rollback",
                  source == kSOSDataSourceSOSTransaction ? "sos" :
                  source == kSOSDataSourceCKKSTransaction ? "ckks" :
                  source == kSOSDataSourceAPITransaction ? "api" :
                  "unknown",
                  changesDesc);
        CFReleaseSafe(changesDesc);
        if (ct->manifest || ct->manifestChildren) {
            SOSManifestRef additions = NULL;
            SOSManifestRef removals = NULL;
            ok &= SOSChangeTrackerCreateManifestsWithChanges(engine, changes, &removals, &additions, error);
            if (ok) {
                if (ct->manifest) {
                    SOSManifestRef updatedManifest = SOSManifestCreateWithPatch(ct->manifest, removals, additions, error);
                    if (updatedManifest){
                        CFTransferRetained(ct->manifest, updatedManifest);
                    }
                }
                if (ct->manifestChildren) {
                    SOSChangeTrackerUpdatesManifests child;
                    CFArrayForEachC(ct->manifestChildren, child) {
                        ok = ok && child(ct, engine, txn, source, phase, removals, additions, error);
                    }
                }
            }
            CFReleaseSafe(removals);
            CFReleaseSafe(additions);
            // TODO: Potentially filter changes to eliminate any changes that were already in our manifest
            // Backup Peers and the like would probably enjoy this so they don't have to do it themselves.
        }

        if (ct->changeChildren) {
            SOSChangeTrackerUpdatesChanges child;
            CFArrayForEachC(ct->changeChildren, child) {
                ok = ok && child(ct, engine, txn, source, phase, changes, error);
            }
        }
    }
    
    return ok;
}

