/*
 Copyright 2015 OpenMarket Ltd
 Copyright 2017 Vector Creations Ltd
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
 You may obtain a copy of the License at
 
 http://www.apache.org/licenses/LICENSE-2.0
 
 Unless required by applicable law or agreed to in writing, software
 distributed under the License is distributed on an "AS IS" BASIS,
 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 See the License for the specific language governing permissions and
 limitations under the License.
 */

#import "MXKTools.h"

@import MatrixSDK;
@import Contacts;
@import libPhoneNumber_iOS;
@import DTCoreText;

#import "MXKConstants.h"
#import "NSBundle+MatrixKit.h"
#import "MXKAppSettings.h"
#import <MatrixSDK/MXTools.h>
#import "MXKSwiftHeader.h"

#pragma mark - Constants definitions

// Temporary background color used to identify blockquote blocks with DTCoreText.
#define kMXKToolsBlockquoteMarkColor [UIColor magentaColor]

// Attribute in an NSAttributeString that marks a blockquote block that was in the original HTML string.
NSString *const kMXKToolsBlockquoteMarkAttribute = @"kMXKToolsBlockquoteMarkAttribute";

// Regex expression for permalink detection
NSString *const kMXKToolsRegexStringForPermalink = @"\\/#\\/(?:(?:room|user)\\/)?([^\\s]*)";


#pragma mark - MXKTools static private members
// The regex used to find matrix ids.
static NSRegularExpression *userIdRegex;
static NSRegularExpression *roomIdRegex;
static NSRegularExpression *roomAliasRegex;
static NSRegularExpression *eventIdRegex;
// A regex to find http URLs.
static NSRegularExpression *httpLinksRegex;
// A regex to find all HTML tags
static NSRegularExpression *htmlTagsRegex;
static NSDataDetector *linkDetector;
// A regex to detect permalinks
static NSRegularExpression* permalinkRegex;

@implementation MXKTools

+ (void)initialize
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        
        userIdRegex = [NSRegularExpression regularExpressionWithPattern:kMXToolsRegexStringForMatrixUserIdentifier options:NSRegularExpressionCaseInsensitive error:nil];
        roomIdRegex = [NSRegularExpression regularExpressionWithPattern:kMXToolsRegexStringForMatrixRoomIdentifier options:NSRegularExpressionCaseInsensitive error:nil];
        roomAliasRegex = [NSRegularExpression regularExpressionWithPattern:kMXToolsRegexStringForMatrixRoomAlias options:NSRegularExpressionCaseInsensitive error:nil];
        eventIdRegex = [NSRegularExpression regularExpressionWithPattern:kMXToolsRegexStringForMatrixEventIdentifier options:NSRegularExpressionCaseInsensitive error:nil];
        
        httpLinksRegex = [NSRegularExpression regularExpressionWithPattern:@"(?i)\\b(https?://\\S*)\\b" options:NSRegularExpressionCaseInsensitive error:nil];
        htmlTagsRegex  = [NSRegularExpression regularExpressionWithPattern:@"<(\\w+)[^>]*>" options:NSRegularExpressionCaseInsensitive error:nil];
        linkDetector = [NSDataDetector dataDetectorWithTypes:NSTextCheckingTypeLink error:nil];
               
        // if we have a custom clientPermalinkBaseUrl, we also need to support matrix.to permalinks
        NSString *permalinkPattern = [NSString stringWithFormat:@"(?:%@|%@)%@", BuildSettings.clientPermalinkBaseUrl, kMXMatrixDotToUrl, kMXKToolsRegexStringForPermalink];
        permalinkRegex = [NSRegularExpression regularExpressionWithPattern:permalinkPattern options:NSRegularExpressionCaseInsensitive error:nil];
    });
}

#pragma mark - Strings

+ (BOOL)isSingleEmojiString:(NSString *)string
{
    return [MXKTools isEmojiString:string singleEmoji:YES];
}

+ (BOOL)isEmojiOnlyString:(NSString *)string
{
    return [MXKTools isEmojiString:string singleEmoji:NO];
}

// Highly inspired from https://stackoverflow.com/a/34659249
+ (BOOL)isEmojiString:(NSString*)string singleEmoji:(BOOL)singleEmoji
{
    if (string.length == 0)
    {
        return NO;
    }

    __block BOOL result = YES;

    NSRange stringRange = NSMakeRange(0, [string length]);

    [string enumerateSubstringsInRange:stringRange
                               options:NSStringEnumerationByComposedCharacterSequences
                            usingBlock:^(NSString *substring,
                                         NSRange substringRange,
                                         NSRange enclosingRange,
                                         BOOL *stop)
     {
         BOOL isEmoji = NO;

         if (singleEmoji && !NSEqualRanges(stringRange, substringRange))
         {
             // The string contains several characters. Go out
             result = NO;
             *stop = YES;
             return;
         }

         const unichar hs = [substring characterAtIndex:0];
         // Surrogate pair
         if (0xd800 <= hs &&
             hs <= 0xdbff)
         {
             if (substring.length > 1)
             {
                 const unichar ls = [substring characterAtIndex:1];
                 const int uc = ((hs - 0xd800) * 0x400) + (ls - 0xdc00) + 0x10000;
                 if (0x1d000 <= uc &&
                     uc <= 0x1f9ff)
                 {
                     isEmoji = YES;
                 }
             }
         }
         else if (substring.length > 1)
         {
             const unichar ls = [substring characterAtIndex:1];
             if (ls == 0x20e3 ||
                 ls == 0xfe0f ||
                 ls == 0xd83c)
             {
                 isEmoji = YES;
             }
         }
         else
         {
             // Non surrogate
             if (0x2100 <= hs &&
                 hs <= 0x27ff)
             {
                 isEmoji = YES;
             }
             else if (0x2B05 <= hs &&
                      hs <= 0x2b07)
             {
                 isEmoji = YES;
             }
             else if (0x2934 <= hs &&
                      hs <= 0x2935)
             {
                 isEmoji = YES;
             }
             else if (0x3297 <= hs &&
                      hs <= 0x3299)
             {
                 isEmoji = YES;
             }
             else if (hs == 0xa9 ||
                      hs == 0xae ||
                      hs == 0x303d ||
                      hs == 0x3030 ||
                      hs == 0x2b55 ||
                      hs == 0x2b1c ||
                      hs == 0x2b1b ||
                      hs == 0x2b50)
             {
                 isEmoji = YES;
             }
         }

         if (!isEmoji)
         {
             result = NO;
             *stop = YES;
         }
     }];

    return result;
}

#pragma mark - Time interval

+ (NSString*)formatSecondsInterval:(CGFloat)secondsInterval
{
    NSMutableString* formattedString = [[NSMutableString alloc] init];
    
    if (secondsInterval < 1)
    {
        [formattedString appendFormat:@"< 1%@", [VectorL10n formatTimeS]];
    }
    else if (secondsInterval < 60)
    {
        [formattedString appendFormat:@"%d%@", (int)secondsInterval, [VectorL10n formatTimeS]];
    }
    else if (secondsInterval < 3600)
    {
        [formattedString appendFormat:@"%d%@ %2d%@", (int)(secondsInterval/60), [VectorL10n formatTimeM],
         ((int)secondsInterval) % 60, [VectorL10n formatTimeS]];
    }
    else if (secondsInterval >= 3600)
    {
        [formattedString appendFormat:@"%d%@ %d%@ %d%@", (int)(secondsInterval / 3600), [VectorL10n formatTimeH],
         ((int)(secondsInterval) % 3600) / 60, [VectorL10n formatTimeM],
         (int)(secondsInterval) % 60, [VectorL10n formatTimeS]];
    }
    [formattedString appendString:@" left"];
    
    return formattedString;
}

+ (NSString *)formatSecondsIntervalFloored:(CGFloat)secondsInterval
{
    NSString* formattedString;

    if (secondsInterval < 0)
    {
        formattedString = [NSString stringWithFormat:@"0%@", [VectorL10n formatTimeS]];
    }
    else
    {
        NSUInteger seconds = secondsInterval;
        if (seconds < 60)
        {
            formattedString = [NSString stringWithFormat:@"%tu%@", seconds, [VectorL10n formatTimeS]];
        }
        else if (secondsInterval < 3600)
        {
            formattedString = [NSString stringWithFormat:@"%tu%@", seconds / 60, [VectorL10n formatTimeM]];
        }
        else if (secondsInterval < 86400)
        {
            formattedString = [NSString stringWithFormat:@"%tu%@", seconds / 3600, [VectorL10n formatTimeH]];
        }
        else
        {
            formattedString = [NSString stringWithFormat:@"%tu%@", seconds / 86400, [VectorL10n formatTimeD]];
        }
    }

    return formattedString;
}

#pragma mark - Phone number

+ (NSString*)msisdnWithPhoneNumber:(NSString *)phoneNumber andCountryCode:(NSString *)countryCode
{
    NSString *msisdn = nil;
    NBPhoneNumber *phoneNb;
    
    if ([phoneNumber hasPrefix:@"+"] || [phoneNumber hasPrefix:@"00"])
    {
        phoneNb = [[NBPhoneNumberUtil sharedInstance] parse:phoneNumber defaultRegion:nil error:nil];
    }
    else
    {
        // Check whether the provided phone number is a valid msisdn.
        NSString *e164 = [NSString stringWithFormat:@"+%@", phoneNumber];
        phoneNb = [[NBPhoneNumberUtil sharedInstance] parse:e164 defaultRegion:nil error:nil];
        
        if (![[NBPhoneNumberUtil sharedInstance] isValidNumber:phoneNb])
        {
            // Consider the phone number as a national one, and use the country code.
            phoneNb = [[NBPhoneNumberUtil sharedInstance] parse:phoneNumber defaultRegion:countryCode error:nil];
        }
    }
    
    if ([[NBPhoneNumberUtil sharedInstance] isValidNumber:phoneNb])
    {
        NSString *e164 = [[NBPhoneNumberUtil sharedInstance] format:phoneNb numberFormat:NBEPhoneNumberFormatE164 error:nil];
        
        if ([e164 hasPrefix:@"+"])
        {
            msisdn = [e164 substringFromIndex:1];
        }
        else if ([e164 hasPrefix:@"00"])
        {
            msisdn = [e164 substringFromIndex:2];
        }
    }
    
    return msisdn;
}

+ (NSString*)readableMSISDN:(NSString*)msisdn
{
    NSString *e164;
    
    if (([e164 hasPrefix:@"+"]))
    {
        e164 = msisdn;
    }
    else
    {
        e164 = [NSString stringWithFormat:@"+%@", msisdn];
    }
    
    NBPhoneNumber *phoneNb = [[NBPhoneNumberUtil sharedInstance] parse:e164 defaultRegion:nil error:nil];
    return [[NBPhoneNumberUtil sharedInstance] format:phoneNb numberFormat:NBEPhoneNumberFormatINTERNATIONAL error:nil];
}

#pragma mark - Hex color to UIColor conversion

+ (UIColor *)colorWithRGBValue:(NSUInteger)rgbValue
{
    return [UIColor colorWithRed:((float)((rgbValue & 0xFF0000) >> 16))/255.0 green:((float)((rgbValue & 0xFF00) >> 8))/255.0 blue:((float)(rgbValue & 0xFF))/255.0 alpha:1.0];
}

+ (UIColor *)colorWithARGBValue:(NSUInteger)argbValue
{
    return [UIColor colorWithRed:((float)((argbValue & 0xFF0000) >> 16))/255.0 green:((float)((argbValue & 0xFF00) >> 8))/255.0 blue:((float)(argbValue & 0xFF))/255.0 alpha:((float)((argbValue & 0xFF000000) >> 24))/255.0];
}

+ (NSUInteger)rgbValueWithColor:(UIColor*)color
{
    CGFloat red, green, blue, alpha;
    
    [color getRed:&red green:&green blue:&blue alpha:&alpha];
    
    NSUInteger rgbValue = ((int)(red * 255) << 16) + ((int)(green * 255) << 8) + (blue * 255);
    
    return rgbValue;
}

+ (NSUInteger)argbValueWithColor:(UIColor*)color
{
    CGFloat red, green, blue, alpha;
    
    [color getRed:&red green:&green blue:&blue alpha:&alpha];
    
    NSUInteger argbValue = ((int)(alpha * 255) << 24) + ((int)(red * 255) << 16) + ((int)(green * 255) << 8) + (blue * 255);
    
    return argbValue;
}

#pragma mark - Image

+ (UIImage*)forceImageOrientationUp:(UIImage*)imageSrc
{
    if ((imageSrc.imageOrientation == UIImageOrientationUp) || (!imageSrc))
    {
        // Nothing to do
        return imageSrc;
    }
    
    // Draw the entire image in a graphics context, respecting the image’s orientation setting
    UIGraphicsBeginImageContext(imageSrc.size);
    [imageSrc drawAtPoint:CGPointMake(0, 0)];
    UIImage *retImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    
    return retImage;
}

+ (MXKImageCompressionSizes)availableCompressionSizesForImage:(UIImage*)image originalFileSize:(NSUInteger)originalFileSize
{
    MXKImageCompressionSizes compressionSizes;
    memset(&compressionSizes, 0, sizeof(MXKImageCompressionSizes));
    
    // Store the original
    compressionSizes.original.imageSize = image.size;
    compressionSizes.original.fileSize = originalFileSize ? originalFileSize : UIImageJPEGRepresentation(image, 0.9).length;
    
    MXLogDebug(@"[MXKTools] availableCompressionSizesForImage: %f %f - File size: %tu", compressionSizes.original.imageSize.width, compressionSizes.original.imageSize.height, compressionSizes.original.fileSize);
    
    compressionSizes.actualLargeSize = MXKTOOLS_LARGE_IMAGE_SIZE;
    
    // Compute the file size for each compression level
    CGFloat maxSize = MAX(compressionSizes.original.imageSize.width, compressionSizes.original.imageSize.height);
    if (maxSize >= MXKTOOLS_SMALL_IMAGE_SIZE)
    {
        compressionSizes.small.imageSize = [MXKTools resizeImageSize:compressionSizes.original.imageSize toFitInSize:CGSizeMake(MXKTOOLS_SMALL_IMAGE_SIZE, MXKTOOLS_SMALL_IMAGE_SIZE) canExpand:NO];
        
        compressionSizes.small.fileSize = (NSUInteger)[MXTools roundFileSize:(long long)(compressionSizes.small.imageSize.width * compressionSizes.small.imageSize.height * 0.20)];
        
        if (maxSize >= MXKTOOLS_MEDIUM_IMAGE_SIZE)
        {
            compressionSizes.medium.imageSize = [MXKTools resizeImageSize:compressionSizes.original.imageSize toFitInSize:CGSizeMake(MXKTOOLS_MEDIUM_IMAGE_SIZE, MXKTOOLS_MEDIUM_IMAGE_SIZE) canExpand:NO];
            
            compressionSizes.medium.fileSize = (NSUInteger)[MXTools roundFileSize:(long long)(compressionSizes.medium.imageSize.width * compressionSizes.medium.imageSize.height * 0.20)];
            
            if (maxSize >= MXKTOOLS_LARGE_IMAGE_SIZE)
            {
                // In case of panorama the large resolution (1024 x ...) is not relevant. We prefer consider the third of the panarama width.
                compressionSizes.actualLargeSize = maxSize / 3;
                if (compressionSizes.actualLargeSize < MXKTOOLS_LARGE_IMAGE_SIZE)
                {
                    compressionSizes.actualLargeSize = MXKTOOLS_LARGE_IMAGE_SIZE;
                }
                else
                {
                    // Keep a multiple of predefined large size
                    compressionSizes.actualLargeSize = floor(compressionSizes.actualLargeSize / MXKTOOLS_LARGE_IMAGE_SIZE) * MXKTOOLS_LARGE_IMAGE_SIZE;
                }
                
                compressionSizes.large.imageSize = [MXKTools resizeImageSize:compressionSizes.original.imageSize toFitInSize:CGSizeMake(compressionSizes.actualLargeSize, compressionSizes.actualLargeSize) canExpand:NO];
                
                compressionSizes.large.fileSize = (NSUInteger)[MXTools roundFileSize:(long long)(compressionSizes.large.imageSize.width * compressionSizes.large.imageSize.height * 0.20)];
            }
            else
            {
                MXLogDebug(@"    - too small to fit in %d", MXKTOOLS_LARGE_IMAGE_SIZE);
            }
        }
        else
        {
            MXLogDebug(@"    - too small to fit in %d", MXKTOOLS_MEDIUM_IMAGE_SIZE);
        }
    }
    else
    {
        MXLogDebug(@"    - too small to fit in %d", MXKTOOLS_SMALL_IMAGE_SIZE);
    }
    
    return compressionSizes;
}


+ (CGSize)resizeImageSize:(CGSize)originalSize toFitInSize:(CGSize)maxSize canExpand:(BOOL)canExpand
{
    if ((originalSize.width == 0) || (originalSize.height == 0))
    {
        return CGSizeZero;
    }
    
    CGSize resized = originalSize;
    
    if ((maxSize.width > 0) && (maxSize.height > 0) && (canExpand || ((originalSize.width > maxSize.width) || (originalSize.height > maxSize.height))))
    {
        CGFloat ratioX = maxSize.width  / originalSize.width;
        CGFloat ratioY = maxSize.height / originalSize.height;
        
        CGFloat scale = MIN(ratioX, ratioY);
        resized.width  *= scale;
        resized.height *= scale;
        
        // padding
        resized.width  = floorf(resized.width  / 2) * 2;
        resized.height = floorf(resized.height / 2) * 2;
    }
    
    return resized;
}

+ (CGSize)resizeImageSize:(CGSize)originalSize toFillWithSize:(CGSize)maxSize canExpand:(BOOL)canExpand
{
    CGSize resized = originalSize;
    
    if ((maxSize.width > 0) && (maxSize.height > 0) && (canExpand || ((originalSize.width > maxSize.width) && (originalSize.height > maxSize.height))))
    {
        CGFloat ratioX = maxSize.width  / originalSize.width;
        CGFloat ratioY = maxSize.height / originalSize.height;
        
        CGFloat scale = MAX(ratioX, ratioY);
        resized.width  *= scale;
        resized.height *= scale;
        
        // padding
        resized.width  = floorf(resized.width  / 2) * 2;
        resized.height = floorf(resized.height / 2) * 2;
    }
    
    return resized;
}

+ (UIImage *)reduceImage:(UIImage *)image toFitInSize:(CGSize)size
{
    return [self reduceImage:image toFitInSize:size useMainScreenScale:NO];
}

+ (UIImage *)reduceImage:(UIImage *)image toFitInSize:(CGSize)size useMainScreenScale:(BOOL)useMainScreenScale
{
    UIImage *resizedImage;
    
    // Check whether resize is required
    if (size.width && size.height)
    {
        CGFloat width = image.size.width;
        CGFloat height = image.size.height;
        
        if (width > size.width)
        {
            height = (height * size.width) / width;
            height = floorf(height / 2) * 2;
            width = size.width;
        }
        if (height > size.height)
        {
            width = (width * size.height) / height;
            width = floorf(width / 2) * 2;
            height = size.height;
        }
        
        if (width != image.size.width || height != image.size.height)
        {
            // Create the thumbnail
            CGSize imageSize = CGSizeMake(width, height);
            
            // Convert first the provided size in pixels
            
            // The scale factor is set to 0.0 to use the scale factor of the device’s main screen.
            CGFloat scale = useMainScreenScale ? 0.0 : 1.0;
            
            UIGraphicsBeginImageContextWithOptions(imageSize, NO, scale);
            
            //            // set to the top quality
            //            CGContextRef context = UIGraphicsGetCurrentContext();
            //            CGContextSetInterpolationQuality(context, kCGInterpolationHigh);
            
            CGRect thumbnailRect = CGRectMake(0, 0, 0, 0);
            thumbnailRect.origin = CGPointMake(0.0,0.0);
            thumbnailRect.size.width  = imageSize.width;
            thumbnailRect.size.height = imageSize.height;
            
            [image drawInRect:thumbnailRect];
            resizedImage = UIGraphicsGetImageFromCurrentImageContext();
            UIGraphicsEndImageContext();
        }
    }
    else
    {
        resizedImage = image;
    }
    
    return resizedImage;
}

+ (UIImage*)resizeImageWithData:(NSData*)imageData toFitInSize:(CGSize)size
{
    // Create the image source
    CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)imageData, NULL);

    // Take the max dimension of size to fit in
    CGFloat maxPixelSize = fmax(size.width, size.height);

    //Create thumbnail options
    CFDictionaryRef options = (__bridge CFDictionaryRef) @{
                                                           (id) kCGImageSourceCreateThumbnailWithTransform : (id)kCFBooleanTrue,
                                                           (id) kCGImageSourceCreateThumbnailFromImageAlways : (id)kCFBooleanTrue,
                                                           (id) kCGImageSourceThumbnailMaxPixelSize : @(maxPixelSize)
                                                           };

    // Generate the thumbnail
    CGImageRef resizedImageRef = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, options);

    UIImage *resizedImage = [[UIImage alloc] initWithCGImage:resizedImageRef];

    CGImageRelease(resizedImageRef);
    CFRelease(imageSource);

    return resizedImage;
}

+ (UIImage*)resizeImage:(UIImage *)image toSize:(CGSize)size
{
    UIImage *resizedImage = image;
    
    // Check whether resize is required
    if (size.width && size.height)
    {
        // Convert first the provided size in pixels
        // The scale factor is set to 0.0 to use the scale factor of the device’s main screen.
        UIGraphicsBeginImageContextWithOptions(size, NO, 0.0);
        
        CGContextRef context = UIGraphicsGetCurrentContext();
        CGContextSetInterpolationQuality(context, kCGInterpolationHigh);
        
        [image drawInRect:CGRectMake(0, 0, size.width, size.height)];
        resizedImage = UIGraphicsGetImageFromCurrentImageContext();
        
        UIGraphicsEndImageContext();
    }
    
    return resizedImage;
}

+ (UIImage*)resizeImageWithRoundedCorners:(UIImage *)image toSize:(CGSize)size
{
    UIImage *resizedImage = image;
    
    // Check whether resize is required
    if (size.width && size.height)
    {
        // Convert first the provided size in pixels
        // The scale factor is set to 0.0 to use the scale factor of the device’s main screen.
        UIGraphicsBeginImageContextWithOptions(size, NO, 0.0);
        
        CGContextRef context = UIGraphicsGetCurrentContext();
        CGContextSetInterpolationQuality(context, kCGInterpolationHigh);
        
        // Add a clip to round corners
        [[UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, size.width, size.height) cornerRadius:size.width/2] addClip];
        
        [image drawInRect:CGRectMake(0, 0, size.width, size.height)];
        resizedImage = UIGraphicsGetImageFromCurrentImageContext();
        
        UIGraphicsEndImageContext();
    }
    
    return resizedImage;
}

+ (UIImage*)paintImage:(UIImage*)image withColor:(UIColor*)color
{
    UIImage *newImage;
    
    const CGFloat *colorComponents = CGColorGetComponents(color.CGColor);
    
    // Create a new image with the same size
    UIGraphicsBeginImageContextWithOptions(image.size, 0, 0);
    
    CGContextRef gc = UIGraphicsGetCurrentContext();
    
    CGRect rect = (CGRect){ .size = image.size};
    
    [image drawInRect:rect
            blendMode:kCGBlendModeNormal
                alpha:1];
    
    // Binarize the image: Transform all colors into the provided color but keep the alpha
    CGContextSetBlendMode(gc, kCGBlendModeSourceIn);
    CGContextSetRGBFillColor(gc, colorComponents[0], colorComponents[1], colorComponents[2], colorComponents[3]);
    CGContextFillRect(gc, rect);
    
    // Retrieve the result into an UIImage
    newImage = UIGraphicsGetImageFromCurrentImageContext();
    
    UIGraphicsEndImageContext();
    
    return newImage;
}

+ (UIImageOrientation)imageOrientationForRotationAngleInDegree:(NSInteger)angle
{
    NSInteger modAngle = angle % 360;
    
    UIImageOrientation orientation = UIImageOrientationUp;
    if (45 <= modAngle && modAngle < 135)
    {
        return UIImageOrientationRight;
    }
    else if (135 <= modAngle && modAngle < 225)
    {
        return UIImageOrientationDown;
    }
    else if (225 <= modAngle && modAngle < 315)
    {
        return UIImageOrientationLeft;
    }
    
    return orientation;
}

static NSMutableDictionary* backgroundByImageNameDict;

+ (UIColor*)convertImageToPatternColor:(NSString*)resourceName backgroundColor:(UIColor*)backgroundColor patternSize:(CGSize)patternSize resourceSize:(CGSize)resourceSize
{
    if (!resourceName)
    {
        return backgroundColor;
    }
    
    if (!backgroundByImageNameDict)
    {
        backgroundByImageNameDict = [[NSMutableDictionary alloc] init];
    }
    
    NSString* key = [NSString stringWithFormat:@"%@ %f %f", resourceName, patternSize.width, resourceSize.width];
    
    UIColor* bgColor = [backgroundByImageNameDict objectForKey:key];
    
    if (!bgColor)
    {
        UIImageView* backgroundView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, patternSize.width, patternSize.height)];
        backgroundView.backgroundColor = backgroundColor;
        
        CGFloat offsetX = (patternSize.width - resourceSize.width) / 2.0f;
        CGFloat offsetY = (patternSize.height - resourceSize.height) / 2.0f;
        
        UIImageView* resourceImageView = [[UIImageView alloc] initWithFrame:CGRectMake(offsetX, offsetY, resourceSize.width, resourceSize.height)];
        resourceImageView.backgroundColor = [UIColor clearColor];
        UIImage *resImage = [UIImage imageNamed:resourceName];
        if (CGSizeEqualToSize(resImage.size, resourceSize))
        {
            resourceImageView.image = resImage;
        }
        else
        {
            resourceImageView.image = [MXKTools resizeImage:resImage toSize:resourceSize];
        }
        
        
        [backgroundView addSubview:resourceImageView];
        
        // Create a "canvas" (image context) to draw in.
        UIGraphicsBeginImageContextWithOptions(backgroundView.frame.size, NO, 0);
        
        // set to the top quality
        CGContextRef context = UIGraphicsGetCurrentContext();
        CGContextSetInterpolationQuality(context, kCGInterpolationHigh);
        [[backgroundView layer] renderInContext: UIGraphicsGetCurrentContext()];
        UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();
        
        
        bgColor = [[UIColor alloc] initWithPatternImage:image];
        [backgroundByImageNameDict setObject:bgColor forKey:key];
    }
    
    return bgColor;
}

#pragma mark - Video Conversion

+ (UIAlertController*)videoConversionPromptForVideoAsset:(AVAsset *)videoAsset
                                           withCompletion:(void (^)(NSString * _Nullable presetName))completion
{
    UIAlertController *compressionPrompt = [UIAlertController alertControllerWithTitle:[VectorL10n attachmentSizePromptTitle]
                                                                               message:[VectorL10n attachmentSizePromptMessage]
                                                                        preferredStyle:UIAlertControllerStyleActionSheet];
    
    CGSize naturalSize = [videoAsset tracksWithMediaType:AVMediaTypeVideo].firstObject.naturalSize;
    
    // Provide 480p as the baseline preset.
    NSString *fileSizeString = [MXKTools estimatedFileSizeStringForVideoAsset:videoAsset withPresetName:AVAssetExportPreset640x480];
    NSString *title = [VectorL10n attachmentSmallWithResolution:@"480p" :fileSizeString];
    [compressionPrompt addAction:[UIAlertAction actionWithTitle:title
                                                          style:UIAlertActionStyleDefault
                                                        handler:^(UIAlertAction * action) {
        // Call the completion with 480p preset.
        completion(AVAssetExportPreset640x480);
    }]];
    
    // Allow 720p when the video exceeds 480p.
    if (naturalSize.height > 480)
    {
        NSString *fileSizeString = [MXKTools estimatedFileSizeStringForVideoAsset:videoAsset withPresetName:AVAssetExportPreset1280x720];
        NSString *title = [VectorL10n attachmentMediumWithResolution:@"720p" :fileSizeString];
        [compressionPrompt addAction:[UIAlertAction actionWithTitle:title
                                                              style:UIAlertActionStyleDefault
                                                            handler:^(UIAlertAction * action) {
            // Call the completion with 720p preset.
            completion(AVAssetExportPreset1280x720);
        }]];
    }
    
    // Allow 1080p when the video exceeds 720p.
    if (naturalSize.height > 720)
    {
        NSString *fileSizeString = [MXKTools estimatedFileSizeStringForVideoAsset:videoAsset withPresetName:AVAssetExportPreset1920x1080];
        NSString *title = [VectorL10n attachmentLargeWithResolution:@"1080p" :fileSizeString];
        [compressionPrompt addAction:[UIAlertAction actionWithTitle:title
                                                              style:UIAlertActionStyleDefault
                                                            handler:^(UIAlertAction * action) {
            // Call the completion with 1080p preset.
            completion(AVAssetExportPreset1920x1080);
        }]];
    }
    
    [compressionPrompt addAction:[UIAlertAction actionWithTitle:[VectorL10n cancel]
                                                          style:UIAlertActionStyleCancel
                                                        handler:^(UIAlertAction * action) {
        // Cancelled. Call the completion with nil.
        completion(nil);
    }]];
    
    return compressionPrompt;
}

+ (NSString *)estimatedFileSizeStringForVideoAsset:(AVAsset *)videoAsset withPresetName:(NSString *)presetName
{
    AVAssetExportSession *exportSession = [AVAssetExportSession exportSessionWithAsset:videoAsset presetName:presetName];
    exportSession.timeRange = CMTimeRangeMake(kCMTimeZero, videoAsset.duration);
    
    return [MXTools fileSizeToString:exportSession.estimatedOutputFileLength];
}

#pragma mark - App permissions

+ (void)checkAccessForMediaType:(NSString *)mediaType
            manualChangeMessage:(NSString *)manualChangeMessage
      showPopUpInViewController:(UIViewController *)viewController
              completionHandler:(void (^)(BOOL))handler
{
    [AVCaptureDevice requestAccessForMediaType:mediaType completionHandler:^(BOOL granted) {

        dispatch_async(dispatch_get_main_queue(), ^{

            if (granted)
            {
                handler(YES);
            }
            else
            {
                // Access not granted to mediaType
                // Display manualChangeMessage
                UIAlertController *alert = [UIAlertController alertControllerWithTitle:nil message:manualChangeMessage preferredStyle:UIAlertControllerStyleAlert];

                // On iOS >= 8, add a shortcut to the app settings (This requires the shared application instance)
                UIApplication *sharedApplication = [UIApplication performSelector:@selector(sharedApplication)];
                if (sharedApplication && UIApplicationOpenSettingsURLString)
                {
                    [alert addAction:[UIAlertAction actionWithTitle:[VectorL10n settings]
                                                                     style:UIAlertActionStyleDefault
                                                                   handler:^(UIAlertAction * action) {
                                                                       
                                                                       NSURL *url = [NSURL URLWithString:UIApplicationOpenSettingsURLString];
                                                                       [sharedApplication performSelector:@selector(openURL:) withObject:url];
                                                                       
                                                                       // Note: it does not worth to check if the user changes the permission
                                                                       // because iOS restarts the app in case of change of app privacy settings
                                                                       handler(NO);
                                                                       
                                                                   }]];
                }
                
                [alert addAction:[UIAlertAction actionWithTitle:[VectorL10n ok]
                                                          style:UIAlertActionStyleDefault
                                                        handler:^(UIAlertAction * action) {
                                                            
                                                            handler(NO);
                                                            
                                                        }]];
                
                [viewController presentViewController:alert animated:YES completion:nil];
            }
            
        });
    }];
}

+ (void)checkAccessForCall:(BOOL)isVideoCall
manualChangeMessageForAudio:(NSString*)manualChangeMessageForAudio
manualChangeMessageForVideo:(NSString*)manualChangeMessageForVideo
 showPopUpInViewController:(UIViewController*)viewController
         completionHandler:(void (^)(BOOL granted))handler
{
    // Check first microphone permission
    [MXKTools checkAccessForMediaType:AVMediaTypeAudio manualChangeMessage:manualChangeMessageForAudio showPopUpInViewController:viewController completionHandler:^(BOOL granted) {

        if (granted)
        {
            // Check camera permission in case of video call
            if (isVideoCall)
            {
                [MXKTools checkAccessForMediaType:AVMediaTypeVideo manualChangeMessage:manualChangeMessageForVideo showPopUpInViewController:viewController completionHandler:^(BOOL granted) {

                    handler(granted);
                }];
            }
            else
            {
                handler(YES);
            }
        }
        else
        {
            handler(NO);
        }
    }];
}

+ (void)checkAccessForContacts:(NSString *)manualChangeMessage
     showPopUpInViewController:(UIViewController *)viewController
             completionHandler:(void (^)(BOOL granted))handler
{
    [self checkAccessForContacts:nil withManualChangeMessage:manualChangeMessage showPopUpInViewController:viewController completionHandler:handler];
}

+ (void)checkAccessForContacts:(NSString *)manualChangeTitle
       withManualChangeMessage:(NSString *)manualChangeMessage
     showPopUpInViewController:(UIViewController *)viewController
             completionHandler:(void (^)(BOOL granted))handler
{
    // Check if the application is allowed to list the contacts
    CNAuthorizationStatus authStatus = [CNContactStore authorizationStatusForEntityType:CNEntityTypeContacts];
    if (authStatus == CNAuthorizationStatusAuthorized)
    {
        handler(YES);
    }
    else if (authStatus == CNAuthorizationStatusNotDetermined)
    {
        // Request address book access
        [[CNContactStore new] requestAccessForEntityType:CNEntityTypeContacts completionHandler:^(BOOL granted, NSError * _Nullable error) {
            
            [MXSDKOptions.sharedInstance.analyticsDelegate trackContactsAccessGranted:granted];
            
            dispatch_async(dispatch_get_main_queue(), ^{
                
                handler(granted);
                
            });
        }];
    }
    else if (authStatus == CNAuthorizationStatusDenied && viewController && manualChangeMessage)
    {
        // Access not granted to the local contacts
        // Display manualChangeMessage
        UIAlertController *alert = [UIAlertController alertControllerWithTitle:manualChangeTitle message:manualChangeMessage preferredStyle:UIAlertControllerStyleAlert];

        [alert addAction:[UIAlertAction actionWithTitle:VectorL10n.cancel
                                                  style:UIAlertActionStyleDefault
                                                handler:^(UIAlertAction * action) {
            
            handler(NO);
            
        }]];
        
        // Add a shortcut to the app settings (This requires the shared application instance)
        UIApplication *sharedApplication = [UIApplication performSelector:@selector(sharedApplication)];
        if (sharedApplication)
        {
            UIAlertAction *settingsAction = [UIAlertAction actionWithTitle:VectorL10n.settings
                                                                     style:UIAlertActionStyleDefault
                                                                   handler:^(UIAlertAction * action) {
                [MXKAppSettings standardAppSettings].syncLocalContactsPermissionOpenedSystemSettings = YES;
                // Wait for the setting to be saved as the app could be killed imminently.
                [[NSUserDefaults standardUserDefaults] synchronize];
                
                NSURL *url = [NSURL URLWithString:UIApplicationOpenSettingsURLString];
                [sharedApplication performSelector:@selector(openURL:) withObject:url];
                
                // Note: it does not worth to check if the user changes the permission
                // because iOS restarts the app in case of change of app privacy settings
                handler(NO);
            }];
            
            [alert addAction: settingsAction];
            alert.preferredAction = settingsAction;
        }
        
        [viewController presentViewController:alert animated:YES completion:nil];
    }
    else
    {
        handler(NO);
    }
}

#pragma mark - HTML processing

+ (void)removeDTCoreTextArtifacts:(NSMutableAttributedString*)mutableAttributedString
{
    // DTCoreText adds a newline at the end of plain text ( https://github.com/Cocoanetics/DTCoreText/issues/779 )
    // or after a blockquote section.
    // Trim trailing whitespace and newlines in the string content
    while ([mutableAttributedString.string hasSuffixCharacterFromSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]])
    {
        [mutableAttributedString deleteCharactersInRange:NSMakeRange(mutableAttributedString.length - 1, 1)];
    }
    
    // New lines may have also been introduced by the paragraph style
    // Make sure the last paragraph style has no spacing
    [mutableAttributedString enumerateAttributesInRange:NSMakeRange(0, mutableAttributedString.length) options:(NSAttributedStringEnumerationReverse) usingBlock:^(NSDictionary *attrs, NSRange range, BOOL *stop) {
        
        if (attrs[NSParagraphStyleAttributeName])
        {
            NSString *subString = [mutableAttributedString.string substringWithRange:range];
            NSArray *components = [subString componentsSeparatedByCharactersInSet:[NSCharacterSet newlineCharacterSet]];
            
            NSMutableDictionary *updatedAttrs = [NSMutableDictionary dictionaryWithDictionary:attrs];
            NSMutableParagraphStyle *paragraphStyle = [updatedAttrs[NSParagraphStyleAttributeName] mutableCopy];
            paragraphStyle.paragraphSpacing = 0;
            updatedAttrs[NSParagraphStyleAttributeName] = paragraphStyle;
            
            if (components.count > 1)
            {
                NSString *lastComponent = components.lastObject;
                
                NSRange range2 = NSMakeRange(range.location, range.length - lastComponent.length);
                [mutableAttributedString setAttributes:attrs range:range2];
                
                range2 = NSMakeRange(range2.location + range2.length, lastComponent.length);
                [mutableAttributedString setAttributes:updatedAttrs range:range2];
            }
            else
            {
                [mutableAttributedString setAttributes:updatedAttrs range:range];
            }
        }
        
        // Check only the last paragraph
        *stop = YES;
    }];
    
    // Image rendering failed on an exception until we replace the DTImageTextAttachments with a simple NSTextAttachment subclass
    // (thanks to https://github.com/Cocoanetics/DTCoreText/issues/863).
    [mutableAttributedString enumerateAttribute:NSAttachmentAttributeName
                                        inRange:NSMakeRange(0, mutableAttributedString.length)
                                        options:0
                                     usingBlock:^(id value, NSRange range, BOOL *stop) {
                                         
                                         if ([value isKindOfClass:DTImageTextAttachment.class])
                                         {
                                             DTImageTextAttachment *attachment = (DTImageTextAttachment*)value;
                                             NSTextAttachment *textAttachment = [[NSTextAttachment alloc] init];
                                             if (attachment.image)
                                             {
                                                 textAttachment.image = attachment.image;
                                                 
                                                 CGRect frame = textAttachment.bounds;
                                                 frame.size = attachment.displaySize;
                                                 textAttachment.bounds = frame;
                                             }
                                             // Note we remove here attachment without image.
                                             NSAttributedString *attrStringWithImage = [NSAttributedString attributedStringWithAttachment:textAttachment];
                                             [mutableAttributedString replaceCharactersInRange:range withAttributedString:attrStringWithImage];
                                         }
                                     }];
}

+ (void)createLinksInMutableAttributedString:(NSMutableAttributedString*)mutableAttributedString forEnabledMatrixIds:(NSInteger)enabledMatrixIdsBitMask
{
    if (!mutableAttributedString)
    {
        return;
    }

    // If enabled, make user id clickable
    if (enabledMatrixIdsBitMask & MXKTOOLS_USER_IDENTIFIER_BITWISE)
    {
        [MXKTools createLinksInMutableAttributedString:mutableAttributedString matchingRegex:userIdRegex];
    }
    
    // If enabled, make room id clickable
    if (enabledMatrixIdsBitMask & MXKTOOLS_ROOM_IDENTIFIER_BITWISE)
    {
        [MXKTools createLinksInMutableAttributedString:mutableAttributedString matchingRegex:roomIdRegex];
    }
    
    // If enabled, make room alias clickable
    if (enabledMatrixIdsBitMask & MXKTOOLS_ROOM_ALIAS_BITWISE)
    {
        [MXKTools createLinksInMutableAttributedString:mutableAttributedString matchingRegex:roomAliasRegex];
    }
    
    // If enabled, make event id clickable
    if (enabledMatrixIdsBitMask & MXKTOOLS_EVENT_IDENTIFIER_BITWISE)
    {
        [MXKTools createLinksInMutableAttributedString:mutableAttributedString matchingRegex:eventIdRegex];
    }
        
    // Permalinks
    NSArray* matches = [httpLinksRegex matchesInString: [mutableAttributedString string] options:0 range: NSMakeRange(0,mutableAttributedString.length)];
    if (matches) {
        for (NSTextCheckingResult *match in matches)
        {
            NSRange matchRange = [match range];

            NSString *link = [mutableAttributedString.string substringWithRange:matchRange];
            // Handle potential permalinks
            if ([permalinkRegex numberOfMatchesInString:link options:0 range:NSMakeRange(0, link.length)]) {
                NSURLComponents *url = [[NSURLComponents new] initWithString:link];
                if (url.URL)
                {
                    [mutableAttributedString addAttribute:NSLinkAttributeName value:url.URL range:matchRange];
                }
            }
        }
    }
    
    // This allows to check for normal url based links (like https://element.io)
    // And set back the default link color
    matches = [linkDetector matchesInString: [mutableAttributedString string] options:0 range: NSMakeRange(0,mutableAttributedString.length)];
    if (matches)
    {
        for (NSTextCheckingResult *match in matches)
        {
            NSRange matchRange = [match range];
            NSURL *matchUrl = [match URL];
            NSURLComponents *url = [[NSURLComponents new] initWithURL:matchUrl resolvingAgainstBaseURL:NO];
            if (url.URL)
            {
                [mutableAttributedString addAttribute:NSForegroundColorAttributeName value:ThemeService.shared.theme.colors.links range:matchRange];
            }
        }
    }
}

+ (void)createLinksInMutableAttributedString:(NSMutableAttributedString*)mutableAttributedString matchingRegex:(NSRegularExpression*)regex
{
    __block NSArray *linkMatches;
    
    // Enumerate each string matching the regex
    [regex enumerateMatchesInString:mutableAttributedString.string
                            options:0
                              range:NSMakeRange(0, mutableAttributedString.length)
                         usingBlock:^(NSTextCheckingResult *match, NSMatchingFlags flags, BOOL *stop) {
        
        // Do not create a link if there is already one on the found match
        __block BOOL hasAlreadyLink = NO;
        [mutableAttributedString enumerateAttributesInRange:match.range
                                                    options:0
                                                 usingBlock:^(NSDictionary<NSString *,id> * _Nonnull attrs, NSRange range, BOOL * _Nonnull stop) {
            
            if (attrs[NSLinkAttributeName])
            {
                hasAlreadyLink = YES;
                *stop = YES;
            }
        }];
        
        // Do not create a link if the match is part of an http link.
        // The http link will be automatically generated by the UI afterwards.
        // So, do not break it now by adding a link on a subset of this http link.
        if (!hasAlreadyLink)
        {
            if (!linkMatches)
            {
                // Search for the links in the string only once
                // Do not use NSDataDetector with NSTextCheckingTypeLink because is not able to
                // manage URLs with 2 hashes like "https://matrix.to/#/#matrix:matrix.org"
                // Such URL is not valid but web browsers can open them and users C+P them...
                // NSDataDetector does not support it but UITextView and UIDataDetectorTypeLink
                // detect them when they are displayed. So let the UI create the link at display.
                linkMatches = [httpLinksRegex matchesInString:mutableAttributedString.string options:0 range:NSMakeRange(0, mutableAttributedString.length)];
            }
            
            for (NSTextCheckingResult *linkMatch in linkMatches)
            {
                // If the match is fully in the link, skip it
                if (NSIntersectionRange(match.range, linkMatch.range).length == match.range.length)
                {
                    // but before we set the right color
                    [mutableAttributedString addAttribute:NSForegroundColorAttributeName value:ThemeService.shared.theme.colors.links range:linkMatch.range];
                    hasAlreadyLink = YES;
                    break;
                }
            }
        }
        
        if (!hasAlreadyLink)
        {
            // Make the link clickable
            // Caution: We need here to escape the non-ASCII characters (like '#' in room alias)
            // to convert the link into a legal URL string.
            NSString *link = [mutableAttributedString.string substringWithRange:match.range];
            link = [link stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];
            [mutableAttributedString addAttribute:NSLinkAttributeName value:link range:match.range];
            [mutableAttributedString addAttribute:NSForegroundColorAttributeName value:ThemeService.shared.theme.colors.links range:match.range];
        }
    }];
}

#pragma mark - HTML processing - blockquote display handling

+ (NSString*)cssToMarkBlockquotes
{
    return [NSString stringWithFormat:@"blockquote {background: #%lX; display: block;}", (unsigned long)[MXKTools rgbValueWithColor:kMXKToolsBlockquoteMarkColor]];
}

+ (void)removeMarkedBlockquotesArtifacts:(NSMutableAttributedString*)mutableAttributedString
{
    // Enumerate all sections marked thanks to `cssToMarkBlockquotes`
    // and apply our own attribute instead.

    // According to blockquotes in the string, DTCoreText can apply 2 policies:
    //     - define a `DTTextBlocksAttribute` attribute on a <blockquote> block
    //     - or, just define a `NSBackgroundColorAttributeName` attribute

    // `DTTextBlocksAttribute` case
    [mutableAttributedString enumerateAttribute:DTTextBlocksAttribute
                                        inRange:NSMakeRange(0, mutableAttributedString.length)
                                        options:0
                                     usingBlock:^(id  _Nullable value, NSRange range, BOOL * _Nonnull stop)
     {
         if ([value isKindOfClass:NSArray.class])
         {
             NSArray *array = (NSArray*)value;
             if (array.count > 0 && [array[0] isKindOfClass:DTTextBlock.class])
             {
                 DTTextBlock *dtTextBlock = (DTTextBlock *)array[0];
                 if ([dtTextBlock.backgroundColor isEqual:kMXKToolsBlockquoteMarkColor])
                 {
                     // Apply our own attribute
                     [mutableAttributedString addAttribute:kMXKToolsBlockquoteMarkAttribute value:@(YES) range:range];

                     // Fix a boring behaviour where DTCoreText add a " " string before a string corresponding
                     // to an HTML blockquote. This " " string has ParagraphStyle.headIndent = 0 which breaks
                     // the blockquote block indentation
                     if (range.location > 0)
                     {
                         NSRange prevRange = NSMakeRange(range.location - 1, 1);

                         NSRange effectiveRange;
                         NSParagraphStyle *paragraphStyle = [mutableAttributedString attribute:NSParagraphStyleAttributeName
                                                                                       atIndex:prevRange.location
                                                                                effectiveRange:&effectiveRange];

                         // Check if this is the " " string
                         if (paragraphStyle && effectiveRange.length == 1 && paragraphStyle.firstLineHeadIndent != 25)
                         {
                             // Fix its paragraph style
                             NSMutableParagraphStyle *newParagraphStyle = [paragraphStyle mutableCopy];
                             newParagraphStyle.firstLineHeadIndent = 25.0;
                             newParagraphStyle.headIndent = 25.0;

                             [mutableAttributedString addAttribute:NSParagraphStyleAttributeName value:newParagraphStyle range:prevRange];
                         }
                     }
                 }
             }
         }
     }];

    // `NSBackgroundColorAttributeName` case
    [mutableAttributedString enumerateAttribute:NSBackgroundColorAttributeName
                                        inRange:NSMakeRange(0, mutableAttributedString.length)
                                        options:0
                                     usingBlock:^(id value, NSRange range, BOOL *stop)
     {

         if ([value isKindOfClass:UIColor.class] && [(UIColor*)value isEqual:[UIColor magentaColor]])
         {
             // Remove the marked background
             [mutableAttributedString removeAttribute:NSBackgroundColorAttributeName range:range];

             // And apply our own attribute
             [mutableAttributedString addAttribute:kMXKToolsBlockquoteMarkAttribute value:@(YES) range:range];
         }
     }];
}

+ (void)enumerateMarkedBlockquotesInAttributedString:(NSAttributedString*)attributedString usingBlock:(void (^)(NSRange range, BOOL *stop))block
{
    [attributedString enumerateAttribute:kMXKToolsBlockquoteMarkAttribute
                                 inRange:NSMakeRange(0, attributedString.length)
                                 options:0
                              usingBlock:^(id  _Nullable value, NSRange range, BOOL * _Nonnull stop)
     {
         if ([value isKindOfClass:NSNumber.class] && ((NSNumber*)value).boolValue)
         {
             block(range, stop);
         }
     }];
}

#pragma mark - Push

// Trim push token before printing it in logs
+ (NSString*)logForPushToken:(NSData*)pushToken
{
    NSUInteger len = ((pushToken.length > 8) ? 8 : pushToken.length / 2);
    return [NSString stringWithFormat:@"%@...", [pushToken subdataWithRange:NSMakeRange(0, len)]];
}

@end
