//
//  FLEXRuntimeUtility.m
//  Flipboard
//
//  Created by Ryan Olson on 6/8/14.
//  Copyright (c) 2014 Flipboard. All rights reserved.
//

#import <UIKit/UIKit.h>
#import "FLEXRuntimeUtility.h"
#import "FLEXObjcInternal.h"
#import "FLEXTypeEncodingParser.h"

// See https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtPropertyIntrospection.html#//apple_ref/doc/uid/TP40008048-CH101-SW6
NSString *const kFLEXPropertyAttributeKeyTypeEncoding = @"T";
NSString *const kFLEXPropertyAttributeKeyBackingIvarName = @"V";
NSString *const kFLEXPropertyAttributeKeyReadOnly = @"R";
NSString *const kFLEXPropertyAttributeKeyCopy = @"C";
NSString *const kFLEXPropertyAttributeKeyRetain = @"&";
NSString *const kFLEXPropertyAttributeKeyNonAtomic = @"N";
NSString *const kFLEXPropertyAttributeKeyCustomGetter = @"G";
NSString *const kFLEXPropertyAttributeKeyCustomSetter = @"S";
NSString *const kFLEXPropertyAttributeKeyDynamic = @"D";
NSString *const kFLEXPropertyAttributeKeyWeak = @"W";
NSString *const kFLEXPropertyAttributeKeyGarbageCollectable = @"P";
NSString *const kFLEXPropertyAttributeKeyOldStyleTypeEncoding = @"t";

static NSString *const FLEXRuntimeUtilityErrorDomain = @"FLEXRuntimeUtilityErrorDomain";
typedef NS_ENUM(NSInteger, FLEXRuntimeUtilityErrorCode) {
    FLEXRuntimeUtilityErrorCodeDoesNotRecognizeSelector = 0,
    FLEXRuntimeUtilityErrorCodeInvocationFailed = 1,
    FLEXRuntimeUtilityErrorCodeArgumentTypeMismatch = 2
};

// Arguments 0 and 1 are self and _cmd always
const unsigned int kFLEXNumberOfImplicitArgs = 2;

@implementation FLEXRuntimeUtility


#pragma mark - General Helpers (Public)

+ (BOOL)pointerIsValidObjcObject:(const void *)pointer {
    return FLEXPointerIsValidObjcObject(pointer);
}

+ (id)potentiallyUnwrapBoxedPointer:(id)returnedObjectOrNil type:(const FLEXTypeEncoding *)returnType {
    if (!returnedObjectOrNil) {
        return nil;
    }

    NSInteger i = 0;
    if (returnType[i] == FLEXTypeEncodingConst) {
        i++;
    }

    BOOL returnsObjectOrClass = returnType[i] == FLEXTypeEncodingObjcObject ||
                                returnType[i] == FLEXTypeEncodingObjcClass;
    BOOL returnsVoidPointer   = returnType[i] == FLEXTypeEncodingPointer &&
                                returnType[i+1] == FLEXTypeEncodingVoid;
    BOOL returnsCString       = returnType[i] == FLEXTypeEncodingCString;

    // If we got back an NSValue and the return type is not an object,
    // we check to see if the pointer is of a valid object. If not,
    // we just display the NSValue.
    if (!returnsObjectOrClass) {
        // Skip NSNumber instances
        if ([returnedObjectOrNil isKindOfClass:[NSNumber class]]) {
            return returnedObjectOrNil;
        }
        
        // Can only be NSValue since return type is not an object,
        // so we bail if this doesn't add up
        if (![returnedObjectOrNil isKindOfClass:[NSValue class]]) {
            return returnedObjectOrNil;
        }

        NSValue *value = (NSValue *)returnedObjectOrNil;

        if (returnsCString) {
            // Wrap char * in NSString
            const char *string = (const char *)value.pointerValue;
            returnedObjectOrNil = string ? [NSString stringWithCString:string encoding:NSUTF8StringEncoding] : NULL;
        } else if (returnsVoidPointer) {
            // Cast valid objects disguised as void * to id
            if ([FLEXRuntimeUtility pointerIsValidObjcObject:value.pointerValue]) {
                returnedObjectOrNil = (__bridge id)value.pointerValue;
            }
        }
    }

    return returnedObjectOrNil;
}

+ (NSUInteger)fieldNameOffsetForTypeEncoding:(const FLEXTypeEncoding *)typeEncoding {
    NSUInteger beginIndex = 0;
    while (typeEncoding[beginIndex] == FLEXTypeEncodingQuote) {
        NSUInteger endIndex = beginIndex + 1;
        while (typeEncoding[endIndex] != FLEXTypeEncodingQuote) {
            ++endIndex;
        }
        beginIndex = endIndex + 1;
    }
    return beginIndex;
}

+ (NSArray<Class> *)classHierarchyOfObject:(id)objectOrClass {
    NSMutableArray<Class> *superClasses = [NSMutableArray new];
    id cls = [objectOrClass class];
    do {
        [superClasses addObject:cls];
    } while ((cls = [cls superclass]));

    return superClasses;
}

/// Could be nil
+ (NSString *)safeDescriptionForObject:(id)object {
    // Don't assume that we have an NSObject subclass.
    // Check to make sure the object responds to the description method
    if ([object respondsToSelector:@selector(description)]) {
        return [object description];
    }

    return nil;
}

/// Never nil
+ (NSString *)safeDebugDescriptionForObject:(id)object {
    NSString *description = nil;

    // Don't assume that we have an NSObject subclass.
    // Check to make sure the object responds to the description method
    if ([object respondsToSelector:@selector(debugDescription)]) {
        description = [object debugDescription];
    } else {
        description = [self safeDescriptionForObject:object];
    }

    if (!description.length) {
        NSString *cls = NSStringFromClass(object_getClass(object));
        if (object_isClass(object)) {
            description = [cls stringByAppendingString:@" class (no description)"];
        } else {
            description = [cls stringByAppendingString:@" instance (no description)"];
        }
    }

    return description;
}

+ (NSString *)summaryForObject:(id)value {
    NSString *description = nil;

    // Special case BOOL for better readability.
    if ([value isKindOfClass:[NSValue class]]) {
        const char *type = [value objCType];
        if (strcmp(type, @encode(BOOL)) == 0) {
            BOOL boolValue = NO;
            [value getValue:&boolValue];
            return boolValue ? @"YES" : @"NO";
        } else if (strcmp(type, @encode(SEL)) == 0) {
            SEL selector = NULL;
            [value getValue:&selector];
            return NSStringFromSelector(selector);
        }
    }

    @try {
        // Single line display - replace newlines and tabs with spaces.
        description = [[self safeDescriptionForObject:value] stringByReplacingOccurrencesOfString:@"\n" withString:@" "];
        description = [description stringByReplacingOccurrencesOfString:@"\t" withString:@" "];
    } @catch (NSException *e) {
        description = [@"Thrown: " stringByAppendingString:e.reason ?: @"(nil exception reason)"];
    }

    if (!description) {
        description = @"nil";
    }

    return description;
}


#pragma mark - Property Helpers (Public)

+ (BOOL)tryAddPropertyWithName:(const char *)name
                    attributes:(NSDictionary<NSString *, NSString *> *)attributePairs
                       toClass:(__unsafe_unretained Class)theClass {
    objc_property_t property = class_getProperty(theClass, name);
    if (!property) {
        unsigned int totalAttributesCount = (unsigned int)attributePairs.count;
        objc_property_attribute_t *attributes = malloc(sizeof(objc_property_attribute_t) * totalAttributesCount);
        if (attributes) {
            unsigned int attributeIndex = 0;
            for (NSString *attributeName in attributePairs.allKeys) {
                objc_property_attribute_t attribute;
                attribute.name = attributeName.UTF8String;
                attribute.value = attributePairs[attributeName].UTF8String;
                attributes[attributeIndex++] = attribute;
            }

            BOOL success = class_addProperty(theClass, name, attributes, totalAttributesCount);
            free(attributes);
            return success;
        } else {
            return NO;
        }
    }
    
    return YES;
}

+ (NSArray<NSString *> *)allPropertyAttributeKeys {
    static NSArray<NSString *> *allPropertyAttributeKeys = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        allPropertyAttributeKeys = @[
            kFLEXPropertyAttributeKeyTypeEncoding,
            kFLEXPropertyAttributeKeyBackingIvarName,
            kFLEXPropertyAttributeKeyReadOnly,
            kFLEXPropertyAttributeKeyCopy,
            kFLEXPropertyAttributeKeyRetain,
            kFLEXPropertyAttributeKeyNonAtomic,
            kFLEXPropertyAttributeKeyCustomGetter,
            kFLEXPropertyAttributeKeyCustomSetter,
            kFLEXPropertyAttributeKeyDynamic,
            kFLEXPropertyAttributeKeyWeak,
            kFLEXPropertyAttributeKeyGarbageCollectable,
            kFLEXPropertyAttributeKeyOldStyleTypeEncoding,
        ];
    });

    return allPropertyAttributeKeys;
}


#pragma mark - Method Helpers (Public)

+ (NSArray<NSString *> *)prettyArgumentComponentsForMethod:(Method)method {
    NSMutableArray<NSString *> *components = [NSMutableArray new];

    NSString *selectorName = NSStringFromSelector(method_getName(method));
    NSMutableArray<NSString *> *selectorComponents = [selectorName componentsSeparatedByString:@":"].mutableCopy;

    // this is a workaround cause method_getNumberOfArguments() returns wrong number for some methods
    if (selectorComponents.count == 1) {
        return @[];
    }

    if ([selectorComponents.lastObject isEqualToString:@""]) {
        [selectorComponents removeLastObject];
    }

    for (unsigned int argIndex = 0; argIndex < selectorComponents.count; argIndex++) {
        char *argType = method_copyArgumentType(method, argIndex + kFLEXNumberOfImplicitArgs);
        NSString *readableArgType = (argType != NULL) ? [self readableTypeForEncoding:@(argType)] : nil;
        free(argType);
        NSString *prettyComponent = [NSString
            stringWithFormat:@"%@:(%@) ",
            selectorComponents[argIndex],
            readableArgType
        ];
        [components addObject:prettyComponent];
    }

    return components;
}


#pragma mark - Method Calling/Field Editing (Public)

+ (id)performSelector:(SEL)selector onObject:(id)object {
    return [self performSelector:selector onObject:object withArguments:@[] error:nil];
}

+ (id)performSelector:(SEL)selector
             onObject:(id)object
        withArguments:(NSArray *)arguments
                error:(NSError * __autoreleasing *)error {
    static dispatch_once_t onceToken;
    static SEL stdStringExclusion = nil;
    dispatch_once(&onceToken, ^{
        stdStringExclusion = NSSelectorFromString(@"stdString");
    });

    // Bail if the object won't respond to this selector.
    if (![object respondsToSelector:selector]) {
        if (error) {
            NSString *msg = [NSString
                stringWithFormat:@"%@ does not respond to the selector %@",
                object, NSStringFromSelector(selector)
            ];
            NSDictionary<NSString *, id> *userInfo = @{ NSLocalizedDescriptionKey : msg };
            *error = [NSError
                errorWithDomain:FLEXRuntimeUtilityErrorDomain
                code:FLEXRuntimeUtilityErrorCodeDoesNotRecognizeSelector
                userInfo:userInfo
            ];
        }

        return nil;
    }

    NSMethodSignature *methodSignature = [NSMethodSignature signatureWithObjCTypes:({
        Method method;
        if (object_isClass(object)) {
            method = class_getClassMethod(object, selector);
        } else {
            method = class_getInstanceMethod(object_getClass(object), selector);
        }
        method_getTypeEncoding(method);
    })];
    
    // Probably an unsupported type encoding, like bitfields.
    // In the future, we could calculate the return length
    // on our own. For now, we abort.
    //
    // For future reference, the code here will get the true type encoding.
    // NSMethodSignature will convert {?=b8b4b1b1b18[8S]} to {?}
    //
    // returnType = method_getTypeEncoding(class_getInstanceMethod([object class], selector));
    if (!methodSignature.methodReturnLength &&
        methodSignature.methodReturnType[0] != FLEXTypeEncodingVoid) {
        return nil;
    }

    // Build the invocation
    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature];
    [invocation setSelector:selector];
    [invocation setTarget:object];
    [invocation retainArguments];

    // Always self and _cmd
    NSUInteger numberOfArguments = [methodSignature numberOfArguments];
    for (NSUInteger argumentIndex = kFLEXNumberOfImplicitArgs; argumentIndex < numberOfArguments; argumentIndex++) {
        NSUInteger argumentsArrayIndex = argumentIndex - kFLEXNumberOfImplicitArgs;
        id argumentObject = arguments.count > argumentsArrayIndex ? arguments[argumentsArrayIndex] : nil;

        // NSNull in the arguments array can be passed as a placeholder to indicate nil.
        // We only need to set the argument if it will be non-nil.
        if (argumentObject && ![argumentObject isKindOfClass:[NSNull class]]) {
            const char *typeEncodingCString = [methodSignature getArgumentTypeAtIndex:argumentIndex];
            if (typeEncodingCString[0] == FLEXTypeEncodingObjcObject ||
              typeEncodingCString[0] == FLEXTypeEncodingObjcClass ||
              [self isTollFreeBridgedValue:argumentObject forCFType:typeEncodingCString]) {
                // Object
                [invocation setArgument:&argumentObject atIndex:argumentIndex];
            } else if (strcmp(typeEncodingCString, @encode(CGColorRef)) == 0 &&
                    [argumentObject isKindOfClass:[UIColor class]]) {
                // Bridging UIColor to CGColorRef
                CGColorRef colorRef = [argumentObject CGColor];
                [invocation setArgument:&colorRef atIndex:argumentIndex];
            } else if ([argumentObject isKindOfClass:[NSValue class]]) {
                // Primitive boxed in NSValue
                NSValue *argumentValue = (NSValue *)argumentObject;

                // Ensure that the type encoding on the NSValue matches the type encoding of the argument in the method signature
                if (strcmp([argumentValue objCType], typeEncodingCString) != 0) {
                    if (error) {
                        NSString *msg =  [NSString
                            stringWithFormat:@"Type encoding mismatch for argument at index %lu. "
                            "Value type: %s; Method argument type: %s.",
                            (unsigned long)argumentsArrayIndex, argumentValue.objCType, typeEncodingCString
                        ];
                        NSDictionary<NSString *, id> *userInfo = @{ NSLocalizedDescriptionKey : msg };
                        *error = [NSError
                            errorWithDomain:FLEXRuntimeUtilityErrorDomain
                            code:FLEXRuntimeUtilityErrorCodeArgumentTypeMismatch
                            userInfo:userInfo
                        ];
                    }
                    return nil;
                }

                @try {
                    NSUInteger bufferSize = 0;
                    FLEXGetSizeAndAlignment(typeEncodingCString, &bufferSize, NULL);

                    if (bufferSize > 0) {
                        void *buffer = alloca(bufferSize);
                        [argumentValue getValue:buffer];
                        [invocation setArgument:buffer atIndex:argumentIndex];
                    }
                } @catch (NSException *exception) { }
            }
        }
    }

    // Try to invoke the invocation but guard against an exception being thrown.
    id returnObject = nil;
    @try {
        [invocation invoke];

        // Retrieve the return value and box if necessary.
        const char *returnType = methodSignature.methodReturnType;

        if (returnType[0] == FLEXTypeEncodingObjcObject || returnType[0] == FLEXTypeEncodingObjcClass) {
            // Return value is an object.
            __unsafe_unretained id objectReturnedFromMethod = nil;
            [invocation getReturnValue:&objectReturnedFromMethod];
            returnObject = objectReturnedFromMethod;
        } else if (returnType[0] != FLEXTypeEncodingVoid) {
            NSAssert(methodSignature.methodReturnLength, @"Memory corruption lies ahead");

            if (returnType[0] == FLEXTypeEncodingStructBegin) {
                if (selector == stdStringExclusion && [object isKindOfClass:[NSString class]]) {
                    // stdString is a C++ object and we will crash if we try to access it
                    if (error) {
                        *error = [NSError
                            errorWithDomain:FLEXRuntimeUtilityErrorDomain
                            code:FLEXRuntimeUtilityErrorCodeInvocationFailed
                            userInfo:@{ NSLocalizedDescriptionKey : @"Skipping -[NSString stdString]" }
                        ];
                    }

                    return nil;
                }
            }

            // Will use arbitrary buffer for return value and box it.
            void *returnValue = malloc(methodSignature.methodReturnLength);
            [invocation getReturnValue:returnValue];
            returnObject = [self valueForPrimitivePointer:returnValue objCType:returnType];
            free(returnValue);
        }
    } @catch (NSException *exception) {
        // Bummer...
        if (error) {
            // "… on <class>" / "… on instance of <class>"
            NSString *class = NSStringFromClass([object class]);
            NSString *calledOn = object == [object class] ? class : [@"an instance of " stringByAppendingString:class];

            NSString *message = [NSString
                stringWithFormat:@"Exception '%@' thrown while performing selector '%@' on %@.\nReason:\n\n%@",
                exception.name, NSStringFromSelector(selector), calledOn, exception.reason
            ];

            *error = [NSError
                errorWithDomain:FLEXRuntimeUtilityErrorDomain
                code:FLEXRuntimeUtilityErrorCodeInvocationFailed
                userInfo:@{ NSLocalizedDescriptionKey : message }
            ];
        }
    }

    return returnObject;
}

+ (BOOL)isTollFreeBridgedValue:(id)value forCFType:(const char *)typeEncoding {
    // See https://developer.apple.com/library/archive/documentation/General/Conceptual/CocoaEncyclopedia/Toll-FreeBridgin/Toll-FreeBridgin.html
#define CASE(cftype, foundationClass) \
    if (strcmp(typeEncoding, @encode(cftype)) == 0) { \
        return [value isKindOfClass:[foundationClass class]]; \
    }

    CASE(CFArrayRef, NSArray);
    CASE(CFAttributedStringRef, NSAttributedString);
    CASE(CFCalendarRef, NSCalendar);
    CASE(CFCharacterSetRef, NSCharacterSet);
    CASE(CFDataRef, NSData);
    CASE(CFDateRef, NSDate);
    CASE(CFDictionaryRef, NSDictionary);
    CASE(CFErrorRef, NSError);
    CASE(CFLocaleRef, NSLocale);
    CASE(CFMutableArrayRef, NSMutableArray);
    CASE(CFMutableAttributedStringRef, NSMutableAttributedString);
    CASE(CFMutableCharacterSetRef, NSMutableCharacterSet);
    CASE(CFMutableDataRef, NSMutableData);
    CASE(CFMutableDictionaryRef, NSMutableDictionary);
    CASE(CFMutableSetRef, NSMutableSet);
    CASE(CFMutableStringRef, NSMutableString);
    CASE(CFNumberRef, NSNumber);
    CASE(CFReadStreamRef, NSInputStream);
    CASE(CFRunLoopTimerRef, NSTimer);
    CASE(CFSetRef, NSSet);
    CASE(CFStringRef, NSString);
    CASE(CFTimeZoneRef, NSTimeZone);
    CASE(CFURLRef, NSURL);
    CASE(CFWriteStreamRef, NSOutputStream);

#undef CASE

    return NO;
}

+ (NSString *)editableJSONStringForObject:(id)object {
    NSString *editableDescription = nil;

    if (object) {
        // This is a hack to use JSON serialization for our editable objects.
        // NSJSONSerialization doesn't allow writing fragments - the top level object must be an array or dictionary.
        // We always wrap the object inside an array and then strip the outer square braces off the final string.
        NSArray *wrappedObject = @[object];
        if ([NSJSONSerialization isValidJSONObject:wrappedObject]) {
            NSData *jsonData = [NSJSONSerialization dataWithJSONObject:wrappedObject options:0 error:NULL];
            NSString *wrappedDescription = [NSString stringWithUTF8String:jsonData.bytes];
            editableDescription = [wrappedDescription substringWithRange:NSMakeRange(1, wrappedDescription.length - 2)];
        }
    }

    return editableDescription;
}

+ (id)objectValueFromEditableJSONString:(NSString *)string {
    id value = nil;
    // nil for empty string/whitespace
    if ([string stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]].length) {
        value = [NSJSONSerialization
            JSONObjectWithData:[string dataUsingEncoding:NSUTF8StringEncoding]
            options:NSJSONReadingAllowFragments
            error:NULL
        ];
    }
    return value;
}

+ (NSValue *)valueForNumberWithObjCType:(const char *)typeEncoding fromInputString:(NSString *)inputString {
    NSNumberFormatter *formatter = [NSNumberFormatter new];
    [formatter setNumberStyle:NSNumberFormatterDecimalStyle];
    NSNumber *number = [formatter numberFromString:inputString];
    
    // Is the type encoding longer than one character?
    if (strlen(typeEncoding) > 1) {
        NSString *type = @(typeEncoding);
        
        // Is it NSDecimalNumber or NSNumber?
        if ([type isEqualToString:@FLEXEncodeClass(NSDecimalNumber)]) {
            return [NSDecimalNumber decimalNumberWithString:inputString];
        } else if ([type isEqualToString:@FLEXEncodeClass(NSNumber)]) {
            return number;
        }
        
        return nil;
    }
    
    // Type encoding is one character, switch on the type
    FLEXTypeEncoding type = typeEncoding[0];
    uint8_t value[32];
    void *bufferStart = &value[0];
    
    // Make sure we box the number with the correct type encoding
    // so it can be properly unboxed later via getValue:
    switch (type) {
        case FLEXTypeEncodingChar:
            *(char *)bufferStart = number.charValue; break;
        case FLEXTypeEncodingInt:
            *(int *)bufferStart = number.intValue; break;
        case FLEXTypeEncodingShort:
            *(short *)bufferStart = number.shortValue; break;
        case FLEXTypeEncodingLong:
            *(long *)bufferStart = number.longValue; break;
        case FLEXTypeEncodingLongLong:
            *(long long *)bufferStart = number.longLongValue; break;
        case FLEXTypeEncodingUnsignedChar:
            *(unsigned char *)bufferStart = number.unsignedCharValue; break;
        case FLEXTypeEncodingUnsignedInt:
            *(unsigned int *)bufferStart = number.unsignedIntValue; break;
        case FLEXTypeEncodingUnsignedShort:
            *(unsigned short *)bufferStart = number.unsignedShortValue; break;
        case FLEXTypeEncodingUnsignedLong:
            *(unsigned long *)bufferStart = number.unsignedLongValue; break;
        case FLEXTypeEncodingUnsignedLongLong:
            *(unsigned long long *)bufferStart = number.unsignedLongLongValue; break;
        case FLEXTypeEncodingFloat:
            *(float *)bufferStart = number.floatValue; break;
        case FLEXTypeEncodingDouble:
            *(double *)bufferStart = number.doubleValue; break;
            
        case FLEXTypeEncodingLongDouble:
            // NSNumber does not support long double
        default:
            return nil;
    }
    
    return [NSValue value:value withObjCType:typeEncoding];
}

+ (void)enumerateTypesInStructEncoding:(const char *)structEncoding
                            usingBlock:(void (^)(NSString *structName,
                                                 const char *fieldTypeEncoding,
                                                 NSString *prettyTypeEncoding,
                                                 NSUInteger fieldIndex,
                                                 NSUInteger fieldOffset))typeBlock {
    if (structEncoding && structEncoding[0] == FLEXTypeEncodingStructBegin) {
        const char *equals = strchr(structEncoding, '=');
        if (equals) {
            const char *nameStart = structEncoding + 1;
            NSString *structName = [@(structEncoding)
                substringWithRange:NSMakeRange(nameStart - structEncoding, equals - nameStart)
            ];

            NSUInteger fieldAlignment = 0, structSize = 0;
            if (FLEXGetSizeAndAlignment(structEncoding, &structSize, &fieldAlignment)) {
                NSUInteger runningFieldIndex = 0;
                NSUInteger runningFieldOffset = 0;
                const char *typeStart = equals + 1;
                
                while (*typeStart != FLEXTypeEncodingStructEnd) {
                    NSUInteger fieldSize = 0;
                    // If the struct type encoding was successfully handled by
                    // FLEXGetSizeAndAlignment above, we *should* be ok with the field here.
                    const char *nextTypeStart = NSGetSizeAndAlignment(typeStart, &fieldSize, NULL);
                    NSString *typeEncoding = [@(structEncoding)
                        substringWithRange:NSMakeRange(typeStart - structEncoding, nextTypeStart - typeStart)
                    ];
                    
                    // Padding to keep proper alignment. __attribute((packed)) structs
                    // will break here. The type encoding is no different for packed structs,
                    // so it's not clear there's anything we can do for those.
                    const NSUInteger currentSizeSum = runningFieldOffset % fieldAlignment;
                    if (currentSizeSum != 0 && currentSizeSum + fieldSize > fieldAlignment) {
                        runningFieldOffset += fieldAlignment - currentSizeSum;
                    }
                    
                    typeBlock(
                        structName,
                        typeEncoding.UTF8String,
                        [self readableTypeForEncoding:typeEncoding],
                        runningFieldIndex,
                        runningFieldOffset
                    );
                    runningFieldOffset += fieldSize;
                    runningFieldIndex++;
                    typeStart = nextTypeStart;
                }
            }
        }
    }
}


#pragma mark - Metadata Helpers

+ (NSDictionary<NSString *, NSString *> *)attributesForProperty:(objc_property_t)property {
    NSString *attributes = @(property_getAttributes(property) ?: "");
    // Thanks to MAObjcRuntime for inspiration here.
    NSArray<NSString *> *attributePairs = [attributes componentsSeparatedByString:@","];
    NSMutableDictionary<NSString *, NSString *> *attributesDictionary = [NSMutableDictionary new];
    for (NSString *attributePair in attributePairs) {
        attributesDictionary[[attributePair substringToIndex:1]] = [attributePair substringFromIndex:1];
    }
    return attributesDictionary;
}

+ (NSString *)appendName:(NSString *)name toType:(NSString *)type {
    if (!type.length) {
        type = @"(?)";
    }
    
    NSString *combined = nil;
    if ([type characterAtIndex:type.length - 1] == FLEXTypeEncodingCString) {
        combined = [type stringByAppendingString:name];
    } else {
        combined = [type stringByAppendingFormat:@" %@", name];
    }
    return combined;
}

+ (NSString *)readableTypeForEncoding:(NSString *)encodingString {
    if (!encodingString.length) {
        return @"?";
    }

    // See https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html
    // class-dump has a much nicer and much more complete implementation for this task, but it is distributed under GPLv2 :/
    // See https://github.com/nygard/class-dump/blob/master/Source/CDType.m
    // Warning: this method uses multiple middle returns and macros to cut down on boilerplate.
    // The use of macros here was inspired by https://www.mikeash.com/pyblog/friday-qa-2013-02-08-lets-build-key-value-coding.html
    const char *encodingCString = encodingString.UTF8String;

    // Some fields have a name, such as {Size=\"width\"d\"height\"d}, we need to extract the name out and recursive
    const NSUInteger fieldNameOffset = [FLEXRuntimeUtility fieldNameOffsetForTypeEncoding:encodingCString];
    if (fieldNameOffset > 0) {
        // According to https://github.com/nygard/class-dump/commit/33fb5ed221810685f57c192e1ce8ab6054949a7c,
        // there are some consecutive quoted strings, so use `_` to concatenate the names.
        NSString *const fieldNamesString = [encodingString substringWithRange:NSMakeRange(0, fieldNameOffset)];
        NSArray<NSString *> *const fieldNames = [fieldNamesString
            componentsSeparatedByString:[NSString stringWithFormat:@"%c", FLEXTypeEncodingQuote]
        ];
        NSMutableString *finalFieldNamesString = [NSMutableString new];
        for (NSString *const fieldName in fieldNames) {
            if (fieldName.length > 0) {
                if (finalFieldNamesString.length > 0) {
                    [finalFieldNamesString appendString:@"_"];
                }
                [finalFieldNamesString appendString:fieldName];
            }
        }
        NSString *const recursiveType = [self readableTypeForEncoding:[encodingString substringFromIndex:fieldNameOffset]];
        return [NSString stringWithFormat:@"%@ %@", recursiveType, finalFieldNamesString];
    }

    // Objects
    if (encodingCString[0] == FLEXTypeEncodingObjcObject) {
        NSString *class = [encodingString substringFromIndex:1];
        class = [class stringByReplacingOccurrencesOfString:@"\"" withString:@""];
        if (class.length == 0 || (class.length == 1 && [class characterAtIndex:0] == FLEXTypeEncodingUnknown)) {
            class = @"id";
        } else {
            class = [class stringByAppendingString:@" *"];
        }
        return class;
    }

    // Qualifier Prefixes
    // Do this first since some of the direct translations (i.e. Method) contain a prefix.
#define RECURSIVE_TRANSLATE(prefix, formatString) \
    if (encodingCString[0] == prefix) { \
        NSString *recursiveType = [self readableTypeForEncoding:[encodingString substringFromIndex:1]]; \
        return [NSString stringWithFormat:formatString, recursiveType]; \
    }

    // If there's a qualifier prefix on the encoding, translate it and then
    // recursively call this method with the rest of the encoding string.
    RECURSIVE_TRANSLATE('^', @"%@ *");
    RECURSIVE_TRANSLATE('r', @"const %@");
    RECURSIVE_TRANSLATE('n', @"in %@");
    RECURSIVE_TRANSLATE('N', @"inout %@");
    RECURSIVE_TRANSLATE('o', @"out %@");
    RECURSIVE_TRANSLATE('O', @"bycopy %@");
    RECURSIVE_TRANSLATE('R', @"byref %@");
    RECURSIVE_TRANSLATE('V', @"oneway %@");
    RECURSIVE_TRANSLATE('b', @"bitfield(%@)");

#undef RECURSIVE_TRANSLATE

  // C Types
#define TRANSLATE(ctype) \
    if (strcmp(encodingCString, @encode(ctype)) == 0) { \
        return (NSString *)CFSTR(#ctype); \
    }

    // Order matters here since some of the cocoa types are typedefed to c types.
    // We can't recover the exact mapping, but we choose to prefer the cocoa types.
    // This is not an exhaustive list, but it covers the most common types
    TRANSLATE(CGRect);
    TRANSLATE(CGPoint);
    TRANSLATE(CGSize);
    TRANSLATE(CGVector);
    TRANSLATE(UIEdgeInsets);
    if (@available(iOS 11.0, *)) {
      TRANSLATE(NSDirectionalEdgeInsets);
    }
    TRANSLATE(UIOffset);
    TRANSLATE(NSRange);
    TRANSLATE(CGAffineTransform);
    TRANSLATE(CATransform3D);
    TRANSLATE(CGColorRef);
    TRANSLATE(CGPathRef);
    TRANSLATE(CGContextRef);
    TRANSLATE(NSInteger);
    TRANSLATE(NSUInteger);
    TRANSLATE(CGFloat);
    TRANSLATE(BOOL);
    TRANSLATE(int);
    TRANSLATE(short);
    TRANSLATE(long);
    TRANSLATE(long long);
    TRANSLATE(unsigned char);
    TRANSLATE(unsigned int);
    TRANSLATE(unsigned short);
    TRANSLATE(unsigned long);
    TRANSLATE(unsigned long long);
    TRANSLATE(float);
    TRANSLATE(double);
    TRANSLATE(long double);
    TRANSLATE(char *);
    TRANSLATE(Class);
    TRANSLATE(objc_property_t);
    TRANSLATE(Ivar);
    TRANSLATE(Method);
    TRANSLATE(Category);
    TRANSLATE(NSZone *);
    TRANSLATE(SEL);
    TRANSLATE(void);

#undef TRANSLATE

    // For structs, we only use the name of the structs
    if (encodingCString[0] == FLEXTypeEncodingStructBegin) {
        // Special case: std::string
        if ([encodingString hasPrefix:@"{basic_string<char"]) {
            return @"std::string";
        }

        const char *equals = strchr(encodingCString, '=');
        if (equals) {
            const char *nameStart = encodingCString + 1;
            // For anonymous structs
            if (nameStart[0] == FLEXTypeEncodingUnknown) {
                return @"anonymous struct";
            } else {
                NSString *const structName = [encodingString
                    substringWithRange:NSMakeRange(nameStart - encodingCString, equals - nameStart)
                ];
                return structName;
            }
        }
    }

    // If we couldn't translate, just return the original encoding string
    return encodingString;
}


#pragma mark - Internal Helpers

+ (NSValue *)valueForPrimitivePointer:(void *)pointer objCType:(const char *)type {
    // Remove the field name if there is any (e.g. \"width\"d -> d)
    const NSUInteger fieldNameOffset = [FLEXRuntimeUtility fieldNameOffsetForTypeEncoding:type];
    if (fieldNameOffset > 0) {
        return [self valueForPrimitivePointer:pointer objCType:type + fieldNameOffset];
    }

    // CASE macro inspired by https://www.mikeash.com/pyblog/friday-qa-2013-02-08-lets-build-key-value-coding.html
#define CASE(ctype, selectorpart) \
    if (strcmp(type, @encode(ctype)) == 0) { \
        return [NSNumber numberWith ## selectorpart: *(ctype *)pointer]; \
    }

    CASE(BOOL, Bool);
    CASE(unsigned char, UnsignedChar);
    CASE(short, Short);
    CASE(unsigned short, UnsignedShort);
    CASE(int, Int);
    CASE(unsigned int, UnsignedInt);
    CASE(long, Long);
    CASE(unsigned long, UnsignedLong);
    CASE(long long, LongLong);
    CASE(unsigned long long, UnsignedLongLong);
    CASE(float, Float);
    CASE(double, Double);
    CASE(long double, Double);

#undef CASE

    NSValue *value = nil;
    if (FLEXGetSizeAndAlignment(type, nil, nil)) {
        @try {
            value = [NSValue valueWithBytes:pointer objCType:type];
        } @catch (NSException *exception) {
            // Certain type encodings are not supported by valueWithBytes:objCType:.
            // Just fail silently if an exception is thrown.
        }
    }

    return value;
}

@end
