//
//  SBRScriptPolicy.m
//  App
//
//  Created by James Magahern on 10/15/21.
//

#import "SBRScriptPolicy.h"

// For icon drawing
#import <UIKit/UIKit.h>

static CGFloat RoundToScale(CGFloat v, CGFloat s) {
    return round(v * s) / s;
}

@implementation SBRScriptPolicy

+ (NSSet<NSString *> *)_commonCDNOrigins {
    static dispatch_once_t onceToken;
    static NSSet<NSString *> *whitelist = nil;
    dispatch_once(&onceToken, ^{
        whitelist = [NSSet setWithArray:@[
            // JSDelivr: Used by bootstrap
            @"cdn.jsdelivr.net",

            // SourceForge
            @"fsdn.net",

            // Open Source CDNs
            @"cdnjs.com",
            @"osscdn.com",

            // JQuery
            @"code.jquery.com",

            // Bootstrap
            @"bootstrapcdn.com",
        ]];
    });
    
    return whitelist;
}

+ (NSString *)titleForPolicyType:(SBRScriptOriginPolicyType)policyType
{
    switch (policyType) {
        case SBRScriptOriginPolicyTypeAlpha:
            return @"Alpha";
        case SBRScriptOriginPolicyTypeBravo:
            return @"Bravo";
        case SBRScriptOriginPolicyTypeCharlie:
            return @"Charlie";
        case SBRScriptOriginPolicyTypeDelta:
            return @"Delta";
        case SBRScriptOriginPolicyTypeEcho:
            return @"Echo";
    }
}

+ (NSString *)localizedDescriptionForPolicyType:(SBRScriptOriginPolicyType)policyType
{
    switch (policyType) {
        case SBRScriptOriginPolicyTypeAlpha:
            return @"All scripts blocked.";
        case SBRScriptOriginPolicyTypeBravo:
            return @"Scripts on page are allowed.";
        case SBRScriptOriginPolicyTypeCharlie:
            return @"Allow scripts from the same security origin.";
        case SBRScriptOriginPolicyTypeDelta:
            return @"Allow scripts from common and host CDNs.";
        case SBRScriptOriginPolicyTypeEcho:
            return @"All scripts are allowed.";
    }
}

+ (UIImage *)iconRepresentationForPolicyType:(SBRScriptOriginPolicyType)policyType withConfiguration:(nonnull SBRScriptPolicyIconConfiguration *)configuration
{
    const CGSize size = [configuration size];
    if (size.width < CGFLOAT_EPSILON || size.height < CGFLOAT_EPSILON) {
        return nil;
    }
    
    UIFont *font = [UIFont boldSystemFontOfSize:size.height - 2];
    NSDictionary<NSAttributedStringKey, id> *attrs = @{
        NSFontAttributeName : font,
        NSForegroundColorAttributeName : [configuration foregroundColor] ?: [UIColor whiteColor],
    };
    
    const CGFloat scale = UIScreen.mainScreen.scale;
    const CGRect rect = (CGRect) { .origin = CGPointZero, .size = size };
    UIGraphicsBeginImageContextWithOptions(size, NO, scale);
    
    [([configuration backgroundColor] ?: [UIColor blackColor]) setFill];
    UIBezierPath *backgroundPath = [UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:4.0];
    [backgroundPath fill];
    
    NSString *character = @"";
    switch (policyType) {
        case SBRScriptOriginPolicyTypeAlpha:
            character = @"𝝰"; break;
        case SBRScriptOriginPolicyTypeBravo:
            character = @"𝝱"; break;
        case SBRScriptOriginPolicyTypeCharlie:
            character = @"𝝲"; break;
        case SBRScriptOriginPolicyTypeDelta:
            character = @"𝝳"; break;
        case SBRScriptOriginPolicyTypeEcho:
            character = @"𝝴"; break;
    }
        
    CGSize charSize = [character sizeWithAttributes:attrs];
    charSize.width = RoundToScale(charSize.width, scale);
    charSize.height = RoundToScale(charSize.height, scale);
    
    const CGRect charRect = (CGRect) {
        .origin = (CGPoint) {
            .x =  (size.width - charSize.width) / 2,
            .y =  -(charSize.height - size.height) / 2,
        },
        
        .size = charSize
    };
    
    [character drawInRect:charRect withAttributes:attrs];
    
    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    
    return image;
}

+ (BOOL)supportsSecureCoding
{
    return YES;
}

- (instancetype)initWithSecurityOrigin:(NSString *)origin policyType:(SBRScriptOriginPolicyType)policyType
{
    if (self = [super init]) {
        _origin = [origin copy];
        _policyType = policyType;
    }
    
    return self;
}

- (instancetype)initWithCoder:(NSCoder *)coder
{
    NSString *origin = [coder decodeObjectForKey:@"origin"];
    NSNumber *policyTypeNumber = [coder decodeObjectForKey:@"policyType"];
    return [self initWithSecurityOrigin:origin policyType:[policyTypeNumber integerValue]];
}

- (void)encodeWithCoder:(NSCoder *)coder
{
    [coder encodeObject:_origin forKey:@"origin"];
    [coder encodeObject:[NSNumber numberWithInteger:_policyType] forKey:@"policyType"];
}

- (BOOL)allowsEmbeddedJavaScript
{
    return _policyType > SBRScriptOriginPolicyTypeAlpha;
}

- (BOOL)allowsExternalJavaScriptResourceOrigin:(NSString *)resourceOrigin
{
    switch (_policyType) {
        case SBRScriptOriginPolicyTypeAlpha:
            // No scripts allowed whatsoever.
            return NO;
        case SBRScriptOriginPolicyTypeBravo:
            // Only allows on-page scripts, no external scripts whatsoever.
            return NO;
        case SBRScriptOriginPolicyTypeCharlie:
            // Only allow scripts from the exact same origin as the host.
            return [_origin isEqualToString:resourceOrigin];
        case SBRScriptOriginPolicyTypeDelta: {
            // Allow scripts from common CDNs, or CDNs that *appear* to belong to the host.
            if ([[[self class] _commonCDNOrigins] containsObject:resourceOrigin]) {
                return YES;
            }
            
            BOOL looksLikeCDN = NO;
            NSArray<NSString *> *hostOriginComponents = [_origin componentsSeparatedByString:@"."];
            if ([hostOriginComponents count] > 1) {
                // Assume our "family" name is just before the TLD.
                NSString *hostFamilyName = [hostOriginComponents objectAtIndex:(hostOriginComponents.count - 2)];
                looksLikeCDN = [resourceOrigin containsString:hostFamilyName];
                
                if (!looksLikeCDN) {
                    NSArray<NSString *> *resourceOriginComponents = [resourceOrigin componentsSeparatedByString:@"."];
                    if ([resourceOriginComponents count] > 1) {
                        NSString *resourceFamilyName = [resourceOriginComponents objectAtIndex:(resourceOriginComponents.count - 2)];
                        
                        // Check and see if the subdomain is "cdn", but also make sure we're not accidentally allowing an ad network CDN.
                        looksLikeCDN = [[resourceOriginComponents firstObject] isEqualToString:@"cdn"];
                        
                        BOOL looksLikeAdNetwork = (
                            [resourceFamilyName hasSuffix:@"ad"] ||
                            [resourceFamilyName hasSuffix:@"ads"] ||
                            [resourceFamilyName containsString:@"analy"]    // "analy-tics"
                        );
                        looksLikeCDN = looksLikeCDN && !looksLikeAdNetwork;
                        
                        NSLog(@"SBRProcessPlugin: [%@] CDN:%@ AD:%@", resourceOriginComponents, looksLikeCDN ? @"YES" : @"NO", looksLikeAdNetwork ? @"YES" : @"NO");
                    }
                }
            }
            
            return looksLikeCDN;
        }
        case SBRScriptOriginPolicyTypeEcho:
            // Anything goes.
            return YES;
    }
}

@end

@implementation SBRScriptPolicyIconConfiguration

- (instancetype)initWithSize:(CGSize)size foregroundColor:(nullable UIColor *)foregroundColor backgroundColor:(nullable UIColor *)backgroundColor
{
    if (self = [super init]) {
        _size = size;
        _foregroundColor = foregroundColor;
        _backgroundColor = backgroundColor;
    }
    
    return self;
}

@end
