/*
 
 Turn back. May God have mercy on the souls of all who venture here.
 
 */

var RasterizeStyle = {
None: 0,
AllExceptShadowAndBorder: 1,  // bit pattern 001
AllExceptShadow: 3,  // bit pattern 011
All: 7 // bit pattern 111
};

var RasterizeBorderMask = 2;
var RasterizeShadowMask = 4;

var doc, export_directory,
export_scale_factor = 1,
assetNumber = 1,
MSSymbolInstanceClassAvailable = (NSClassFromString(@"MSSymbolInstance") != nil),
MSTextLayerClassAvailable = (NSClassFromString(@"MSTextLayer") != nil),
idToMasterMap = [[NSMutableDictionary alloc] init];

//this is a workaround for the radius syntax change in Sketch 42 - '36781' is the build bumber for Sketch 42
var radiusTextDivider = ([[[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleVersion"] integerValue] >= 36781) ? ";" : "/";

function backgroundColor(layer) {
    return [layer respondsToSelector:NSSelectorFromString("backgroundColor")] ? [layer backgroundColor] : [layer backgroundColorGeneric];
}

function get_blur(style) {
    if ([style respondsToSelector: NSSelectorFromString("blur")]) {
        return [style blur];
    }
    
    if ([style respondsToSelector: NSSelectorFromString("blurGeneric")]) {
        return [style blurGeneric];
    }
    
    return null;
}

function exportDirect(exportPath, assetScale, shouldImportSelectedOnly) { // Called Externally
    if (assetScale <= 0) {
        log("Export Scale Factor <= 0. Aborting Export");
        return;
    }
    
    export_scale_factor = assetScale;
    
    doc = [[[NSApplication sharedApplication] orderedDocuments] firstObject];
    [doc showMessage:"Exporting to Principle..."];
    
    export_directory = exportPath;
    [[NSFileManager defaultManager] createDirectoryAtPath:export_directory withIntermediateDirectories:true attributes:null error:null];
    
    var layers;
    var current_page = [doc currentPage];
    var selectedLayers = [current_page selectedLayers];
    var selection = [selectedLayers isKindOfClass: [NSArray class]] ? selectedLayers : [selectedLayers layers];
    
    if (shouldImportSelectedOnly == 1) {
        var artboards = [NSHashTable hashTableWithOptions:(NSHashTableObjectPointerPersonality | NSHashTableStrongMemory)];
        for (var i = 0; i < [selection count]; i++) {
            [artboards addObject: [[selection objectAtIndex:i] parentArtboard]];
        }
        layers = [artboards allObjects];
    }
    
    if (!layers || [layers count] == 0) {
        layers = [current_page layers];
    }
    
    //refresh sizes of groups - this currently only works for v43 and earlier
    if ([current_page respondsToSelector:NSSelectorFromString("selectLayers:")]) {
        [current_page selectLayers: selection];
    }
    
    
    var artboards = flatMap(layers, function(layer) {
                            var canExport = [layer isMemberOfClass:[MSArtboardGroup class]] || isSymbolMaster(layer);
                            return canExport ? layer: null;
                            });
    var index = 1;
    var principleNotificationName = "com.hoopersoftware.principle.import.progressupdate";
    function artboardToMetaData(layer) {
        var includeBGColor = [layer includeBackgroundColorInExport];
        [layer setIncludeBackgroundColorInExport:false];
        [doc showMessage:"Exporting Artboard '"+[layer name]+"' to Principle..."];
        log("- New Artboard -");
        var progressInfo = {imported: index, total: [artboards count], artboardName: [layer name]};
        [[NSDistributedNotificationCenter defaultCenter] postNotificationName:principleNotificationName object:null userInfo:progressInfo deliverImmediately:true];
        index+=1;
        
        var result = process_layer(layer, "", false, false);
        
        [layer setIncludeBackgroundColorInExport:includeBGColor];
        
        return result;
    }
    
    var layers_metadata = flatMap(artboards, artboardToMetaData);
    
    // Write settings
    var settings = [[NSMutableDictionary alloc] init]; // for data.json file
    [settings setValue:export_scale_factor forKey:@"scale"];
    [settings setValue:layers_metadata forKey:@"layers"];
    
    var error = MOPointer.alloc().init()
    var scaleJSON = [NSJSONSerialization dataWithJSONObject:settings options:0 error:error];
    if (error.value) {
        log (error.value());
    }
    var settings_filepath = export_directory + "/data.json";
    [scaleJSON writeToFile: settings_filepath atomically:true];
    
    [doc showMessage:"Finished Exporting to Principle"];
    
}

function enabledItemsForCollection(stylePartCollection) {
    return filter(getAsArray(stylePartCollection), function(el) {return [el isEnabled]});
}

function process_layer(layer, uuidStack, parent_flipped_horizontal, parent_flipped_vertical) {
    if (   ![layer isMemberOfClass:[MSShapeGroup class]]
        && ![layer isMemberOfClass:[MSArtboardGroup class]]
        && ![layer isMemberOfClass:[MSLayerGroup class]]
        && ![layer isMemberOfClass:[MSBitmapLayer class]]
        && !isSymbolMaster(layer)
        && !isSymbolInstance(layer)
        && !isTextLayer(layer)) {
        return;
    }
    
    log("Processing Layer: "+[layer name]);
    
    var shouldIgnoreLayer = ([layer name].toLowerCase().indexOf("principle skip") != -1) || ![layer isVisible];
    if (shouldIgnoreLayer) { return; }
    
    var remove_after_processed = false;
    var originalLayer = layer;
    
    if (isSymbolInstance(layer)) {
        layer = detachSymbolTree(layer, null, false);
        remove_after_processed = true;
        
        if (layer) {
            [originalLayer setIsVisible: false];
        }
    }
    
    if (!layer) {
        return;
    }
    
    // workaround for sketch 39 making fixed width layers really wide.
    if (isTextLayer(layer) && [layer respondsToSelector:NSSelectorFromString("adjustFrameToFit")]) {
        var immutable = [[MSImmutableTextLayer alloc] init];
        [immutable performInitWithMutableModelObject: layer];
        if ([immutable textContainerSize].width < influenceRectForFrame(layer).size.width ) {
            [layer adjustFrameToFit];
        }
    }
    
    var imagePath, sublayers, layers_holder, sub, newUUIDStack = uuidStack;
    
    var style = [layer style];
    
    var tmpName =  [layer name].replace("principle flatten","").replace("@2x","").replace("@3x","").replace("@1x","");
    tmpName = [NSString stringWithString:tmpName];
    if (![tmpName dataUsingEncoding: NSUTF8StringEncoding allowLossyConversion:false]) { // if the layer name has unprintable characters then this will catch it and prevent the entire import from failing
        tmpName = "";
    }
    
    var layer_data = {
    name: tmpName,
    opacity: [[style contextSettings] opacity],
    };
    
    if ([layer isLocked]) {
        layer_data.locked = true;
    }
    
    var sublayers = get_sublayers(layer);
    
    layer_data["id"] = (uuidStack + [layer objectID]);
    var layer_master = [idToMasterMap objectForKey: [layer objectID]];
    if (layer_master) {
        newUUIDStack = layer_data["id"] + "-";
    }
    
    function calculateRasterStyle() {
        // check for reasons to rasterize all layer properties //
        
        var name_requests_flattening = ([layer name].toLowerCase().indexOf("principle flatten") != -1);
        if (name_requests_flattening) {
            return RasterizeStyle.All; // layer name requests flattening
        }
        
        var blur = get_blur(style);
        if (blur && [blur isEnabled]) {
            return RasterizeStyle.All; // blur enabled
        }
        
        for (var j = 0; j < [sublayers count]; j++) {
            if ([[sublayers objectAtIndex:j] isMasked]) {
                return RasterizeStyle.All; // flatten groups containing a mask
            }
        }
        
        var fillColor = null, radius = null;
        
        if ([layer isMemberOfClass:[MSLayerGroup class]] && !layer_master) {
            // do nothing, must have this case to avoid else below
        } else if (layer_master) {
            if ([layer_master includeBackgroundColorInInstance]) {
                fillColor = jsonObjectForColor(backgroundColor(layer_master));
            }
        } else if ([layer isMemberOfClass:[MSArtboardGroup class]] || isSymbolMaster(layer)) {
            fillColor = [layer hasBackgroundColor] ? jsonObjectForColor(backgroundColor(layer)) : {r:1,g:1,b:1};
            fillColor.a = 1;
        } else if ([layer isMemberOfClass:[MSShapeGroup class]]) {
            var pathLayers = [layer layers];
            if ([pathLayers count] != 1) {
                return RasterizeStyle.All; // multiple paths
            }
            
            var shape = [pathLayers firstObject];
            if ([shape respondsToSelector:NSSelectorFromString("edited")] && [shape edited]) {
                return RasterizeStyle.All; // edited vectors cannot be represented by a rounded rect
            }
            
            if ([shape isMemberOfClass:[MSRectangleShape class]]) {
                if ([[shape cornerRadiusString] containsString: radiusTextDivider]) {
                    return RasterizeStyle.All; // rasterizing b/c it has multiple radii
                }
                radius = [shape cornerRadiusFloat];
            } else if ([shape isMemberOfClass:[MSOvalShape class]]) {
                var frame = [shape frame]
                if (Math.abs([frame width] - [frame height]) >= 1) {
                    return RasterizeStyle.All; // rasterizing b/c it's not a circle
                }
                radius = [frame width]/2;
            } else {
                return RasterizeStyle.All; // rasterizing unknown shape class
            }
        } else {
            return RasterizeStyle.All; // rasterizing unknown layer class
        }
        
        var enabledShadows = enabledItemsForCollection([style shadows]);
        if (enabledShadows.length > 1) {
            return RasterizeStyle.All; // multiple shadows
        }
        
        if (enabledShadows.length == 1) {
            var shadowInfo = enabledShadows[0];
            if ([shadowInfo spread] != 0) {
                return RasterizeStyle.All; // shadow spread > 0
            }
            
            layer_data.shadow = {
            color: jsonObjectForColor([shadowInfo color]),
            x:[shadowInfo offsetX],
            y:[shadowInfo offsetY],
                blur:[shadowInfo blurRadius]};
        }
        
        // check for reasons to rasterize border //
        
        var enabledBorders = enabledItemsForCollection([style borders]);
        if (enabledBorders.length > 1) {
            return RasterizeStyle.AllExceptShadow; // multiple borders
        }
        
        // MSArtboardGroup check for Sketch 39 bug: all artboards had an invisible border
        // MSLayerGroup check for Sketch bug: some groups have an invisible border
        if (enabledBorders.length != 0 && ![layer isMemberOfClass:[MSArtboardGroup class]] && ![layer isMemberOfClass:[MSLayerGroup class]]) {
            if (hasBorderStartEnd(style) || [[style borderOptions] hasDashPattern]) {
                return RasterizeStyle.AllExceptShadow; // has arrow or border is dashed
            }
            
            if (enabledBorders.length == 1) {
                var firstBorder = enabledBorders[0];
                if (([firstBorder position] != 1 || [firstBorder fillType] != 0)) {
                    return RasterizeStyle.AllExceptShadow; // has border that is not inside and solid
                }
                layer_data.border = jsonObjectForBorder(firstBorder);
            }
        }
        
        // check for reasons to rasterize fill //
        
        if (enabledItemsForCollection([style innerShadows]).length > 0) {
            return RasterizeStyle.AllExceptShadowAndBorder; // has inner shadows
        }
        
        var enabledFills = enabledItemsForCollection([style fills]);
        if (enabledFills.length > 1) {
            return RasterizeStyle.AllExceptShadowAndBorder; // multiple fills
        }
        
        if ([layer isMemberOfClass:[MSShapeGroup class]] && enabledFills.length == 1) {
            var firstFill = enabledFills[0];
            if ([firstFill fillType] != 0) {
                return RasterizeStyle.AllExceptShadowAndBorder; // rasterizing because of fill type
            }
            
            var newfillcolor = ([firstFill respondsToSelector:NSSelectorFromString("color")] ? [firstFill color] : [firstFill colorGeneric]);
            fillColor = jsonObjectForColor(newfillcolor);
        }
        
        if (fillColor) {
            layer_data.fillColor = fillColor;
        }
        
        if (radius) {
            layer_data.radius = radius;
        }
        
        return RasterizeStyle.None;
    }
    var rasterize = calculateRasterStyle();
    
    var is_flipped_horizontal = parent_flipped_horizontal != [layer isFlippedHorizontal];
    var is_flipped_vertical = parent_flipped_vertical != [layer isFlippedVertical];
    
    var frame = [layer frame];
    layer_data.x = [frame x];
    layer_data.y = [frame y];
    
    if (rasterize) {
        layer_data.fillColor = {r:1,g:1,b:1,a:0};
        
        
        var itemsToEnable = [];
        function disableItems(items) {
            var itemArray = getAsArray(items);
            
            for (var i = 0; i < [itemArray count]; i++) {
                var item = [itemArray objectAtIndex:i];
                if ([item isEnabled]) {
                    itemsToEnable.push(item);
                    [item setIsEnabled:false];
                }
            }
        }
        
        
        if ((rasterize & RasterizeBorderMask) == 0) {
            disableItems([[layer style] borders]);
        }
        
        if ((rasterize & RasterizeShadowMask) == 0) {
            disableItems([[layer style] shadows]);
        }
        
        //influenceRectForFrame returns wrong frame when shapes intersect and opacity is zero, this fixes that
        //influenceRectForFrame returns wrong frame when rotation is set
        var originalOpacity = [[[layer style] contextSettings] opacity];
        var originalRotation = [layer rotation];
        [[[layer style] contextSettings] setOpacity:1];
        [layer setRotation:0];
        
        var influenceFrame = influenceRectForFrame(layer);
        
        [layer setRotation:originalRotation];
        [[[layer style] contextSettings] setOpacity:originalOpacity];
        
        layer_data.image = export_layer(layer, is_flipped_horizontal, is_flipped_vertical);
        var image = [[NSImage alloc] initWithContentsOfFile:layer_data.image];
        if (image != null) {
            layer_data.w = [image size].width / export_scale_factor;
            layer_data.h = [image size].height / export_scale_factor;
        } else {
            layer_data.w = influenceFrame.size.width;
            layer_data.h = influenceFrame.size.height;
        }
        
        if (Math.floor([frame x]) != influenceFrame.origin.x) {
            layer_data.x = influenceFrame.origin.x + (influenceFrame.size.width - layer_data.w)/2);
        }
        
        if (Math.floor([frame y]) != influenceFrame.origin.y) {
            layer_data.y = influenceFrame.origin.y + (influenceFrame.size.height - layer_data.h)/2);
        }
        
        for (var i = 0; i < [itemsToEnable count]; i++) {
            var theItemToEnable = itemsToEnable[i];
            [theItemToEnable setIsEnabled:true];
        }
    } else {
        layer_data.w = [frame width];
        layer_data.h = [frame height];
        
        layers_holder = [];
        
        for (var i = 0; i < [sublayers count]; i++) {
            layers_holder.pushNonNull(process_layer([sublayers objectAtIndex:i], newUUIDStack, is_flipped_horizontal, is_flipped_vertical))
        }
        
        if (layers_holder.length > 0) {
            layer_data.layers = layers_holder;
        }
    }
    
    if ([layer rotation] != 0) {
        if (is_flipped_horizontal == is_flipped_vertical) {
            layer_data.angle = -[layer rotation];
        } else {
            layer_data.angle = [layer rotation];
        }
    }
    
    var parent_frame = [[layer parentGroup] frame];
    if (parent_flipped_horizontal) {
        layer_data.x = [parent_frame width] - layer_data.x - layer_data.w;
    }
    
    if (parent_flipped_vertical) {
        layer_data.y = [parent_frame height] - layer_data.y - layer_data.h;
    }
    
    if (remove_after_processed) {
        [layer removeFromParent];
        [originalLayer setIsVisible: true];
    }
    
    return layer_data;
}

function get_sublayers(layer) {
    if (!layer) {
        return [];
    }
    
    if ([layer isMemberOfClass:[MSLayerGroup class]] || [layer isMemberOfClass:[MSArtboardGroup class]]) {
        return [layer layers];
    } else if (isSymbolInstance(layer)) {
        return getAsArray([[layer symbolMaster] layers]);
    } else if (isSymbolMaster(layer)) {
        return getAsArray([layer layers]);
    }
    
    return [];
}

function detachSymbolTree(layer, overrides, removeSymbol) {
    var override = null;
    if (overrides != null) {
        
        for (var i = 0; i < [overrides count]; i++) {
            override = overrides[i][[layer objectID]];
            if (override != null) {
                if ([override isKindOfClass:[NSDictionary class]]) {
                    var dic = override;
                    override = dic["symbolID"];
                    
                    if (override && (!(typeof override.isKindOfClass === 'function') || ![override isKindOfClass:[NSString class]])) {
                        override = [dic objectForKey:@"symbolID"];
                    }
                }
                
                break;
            }
        }
    }
    
    if (isSymbolInstance(layer)) {
        var master = [layer symbolMaster];
        if (override && override.isKindOfClass && [override isKindOfClass:[NSString class]]) {
            if (![@"" isEqual:override]) {
                master = [[doc documentData] symbolWithID: override];
            } else {
                master = null;
            }
        }
        
        if (master == null) { return null; }
        
        [idToMasterMap setObject:master forKey:[layer objectID]];
        
        var newGroup = [[MSLayerGroup alloc] init];
        newGroup.name = [layer name];
        newGroup.resizingType = [layer resizingType];
        newGroup.objectID = [layer objectID];
        newGroup.frame = [[layer frame] copy];
        newGroup.style = [[layer style] copy];
        newGroup.rotation = [layer rotation];
        newGroup.isFlippedHorizontal = [layer isFlippedHorizontal];
        newGroup.isFlippedVertical = [layer isFlippedVertical];
        [newGroup setIsVisible: [layer isVisible]];
        
        var originalParent = [layer parentGroup];
        [originalParent insertLayer:newGroup atIndex:[originalParent indexOfLayer:layer]];
        
        //add sublayers
        var sublayers = [master layers];
        
        for (var k = 0; k < [sublayers count]; k++) {
            var originalSublayer = sublayers[k];
            var newSublayer = [originalSublayer copy];
            
            [newGroup addLayer:newSublayer];
            copyIDsFromLayerTree(originalSublayer, newSublayer);
        }
        
        //resize layers
        var old_size = CGSizeMake([[master frame] width], [[master frame] height]);
        if ([newGroup respondsToSelector:NSSelectorFromString("resizeChildrenFromOldSize:")]) {
            [newGroup resizeChildrenFromOldSize:old_size];
        } else if ([newGroup respondsToSelector:NSSelectorFromString("resizeChildrenWithOldSize:")]) {
            [newGroup resizeChildrenWithOldSize:old_size];
        } else if ([newGroup respondsToSelector:NSSelectorFromString("resizeChildLayer:oldParentSize:")]){
            var group_sublayers = get_sublayers(newGroup);
            for (var i = 0; i < [group_sublayers count]; i++) {
                [newGroup resizeChildLayer:[group_sublayers objectAtIndex:i] oldParentSize:old_size];
            }
        }
        
        var updatedOverrides = [[NSMutableArray alloc] init];
        
        if (overrides) {
            for (var q = 0; q < [overrides count]; q++) {
                var dictionaryToAdd = overrides[q][[layer objectID]];
                if (dictionaryToAdd != null) {
                    [updatedOverrides addObject: dictionaryToAdd];
                }
            }
        }
        
        var localOverrides = [layer overrides];
        if (localOverrides != null) {
            var overridesToAdd = [localOverrides objectForKey: 0] || localOverrides; //Sketch 44 removed '0' key from dictionary
            [updatedOverrides addObject: overridesToAdd];
        }
        
        overrides = updatedOverrides;
        
        if (removeSymbol) {
            [layer removeFromParent];
        }
        
        layer = newGroup;
    } else if (override != null) {
        if (isTextLayer(layer) && [override isKindOfClass:[NSString class]]) {
            [layer setStringValue: override];
        } else if ([override isMemberOfClass: [MSImageData class]]) {
            if ([layer isMemberOfClass: [MSBitmapLayer class]]) {
                layer.image = override;
            } else if ([layer isMemberOfClass:[MSShapeGroup class]]) {
                var enabledFills = enabledItemsForCollection([[layer style] fills]);
                for (var p = ([enabledFills count] - 1);p >= 0; p--) {
                    var fill = enabledFills[p];
                    if (fill.image != null) {
                        fill.image = override;
                        break;
                    }
                }
            }
        }
    }
    
    //detach sublayers, remove layers that cannot be detached
    var sublayers = get_sublayers(layer);
    for (var s = 0; s < [sublayers count]; s++) {
        var detachedLayer = detachSymbolTree(sublayers[s], overrides, true);
        if (!detachedLayer) {
            [(sublayers[s]) removeFromParent];
            s--;
        }
    }
    
    return layer;
}

function copyIDsFromLayerTree(source_layer, destination_layer) {
    destination_layer.objectID = [source_layer objectID];
    
    if (isSymbolInstance(destination_layer)) {
        return;
    }
    
    var destination_sublayers = get_sublayers(destination_layer);
    var source_sublayers = get_sublayers(source_layer);
    var sublayer_count = Math.min([destination_sublayers count], [source_sublayers count]);
    
    for (var s = 0; s < sublayer_count; s++) {
        copyIDsFromLayerTree(source_sublayers[s], destination_sublayers[s]);
    }
}

function isSymbolMaster(layer) {
    return MSSymbolInstanceClassAvailable && [layer isMemberOfClass:[MSSymbolMaster class]];
}

function isSymbolInstance(layer) {
    return MSSymbolInstanceClassAvailable && [layer isMemberOfClass:[MSSymbolInstance class]];
}

function isTextLayer(layer) {
    return MSTextLayerClassAvailable && [layer isMemberOfClass:[MSTextLayer class]];
}

function export_layer(layer, is_flipped_horizontal, is_flipped_vertical) {
    var path_to_file = export_directory + "/assets/" + assetNumber + ".png";
    assetNumber++;
    
    var layer_to_render = layer;
    
    var style = [layer style];
    var blur_nullable = get_blur(style);
    var render_in_place = (blur_nullable && [blur_nullable type] > 2 && [blur_nullable isEnabled]);
    
    var artboard = [layer_to_render parentArtboard];
    var background_layer = null;
    if (render_in_place) {
        var rect_shape = [[MSRectangleShape alloc] init];
        var frame = [[artboard frame] copy];
        [frame setX: 0];
        [frame setY: 0];
        rect_shape.frame = frame;
        
        background_layer = [MSShapeGroup shapeWithPath: rect_shape];
        var style_fill = [[MSStyleFill alloc] init];
        style_fill.color = [artboard hasBackgroundColor] ? backgroundColor(artboard) : [MSColor colorWithNSColor: [NSColor whiteColor]];
        
        var backgroundStyle = [background_layer style];
        var backgroundFills = [backgroundStyle fills];
        var newFillArray = [NSArray arrayWithObject: style_fill];
        
        if ([backgroundFills isKindOfClass: [NSArray class]]) {
            [backgroundStyle setFills: newFillArray];
        } else if ([backgroundFills respondsToSelector:NSSelectorFromString("setArray:")]) {
            [backgroundFills setArray: newFillArray];
        }
        
        if ([artboard respondsToSelector:NSSelectorFromString("insertLayers:atIndex:")]) {
            [artboard insertLayers: [NSArray arrayWithObject: background_layer] atIndex: 0];
        } else if ([artboard respondsToSelector:NSSelectorFromString("insertLayer:atIndex:")]) {
            [artboard insertLayer: background_layer atIndex: 0];
        }
    } else {
        layer_to_render = [layer duplicate];
        [layer_to_render removeFromParent];
        [layer_to_render setShouldBreakMaskChain:true]; // fix masks outside of artboards from completely hiding the rendered layer
        [[doc currentPage] addLayers: [layer_to_render]];
        [layer_to_render setIsVisible:true];
        
        var frame = [layer_to_render frame];
        [frame setX: -999999];
        [frame setY: -999999];
        
        [layer_to_render setRotation: 0];
        [layer_to_render setIsFlippedHorizontal: is_flipped_horizontal];
        [layer_to_render setIsFlippedVertical: is_flipped_vertical];
    }
    
    //influenceRectForFrame returns wrong frame when shapes intersect and opacity is zero, this fixes that
    var originalOpacity = [[style contextSettings] opacity];
    [[[layer_to_render style] contextSettings] setOpacity:1];
    
    var abs_influence_rect = [layer_to_render absoluteInfluenceRect];
    
    var exportRequest;
    if ([[MSExportRequest class] respondsToSelector:NSSelectorFromString("requestWithRect:scale:")]) {
        exportRequest = [MSExportRequest requestWithRect:abs_influence_rect scale:export_scale_factor];
    } else {
        exportRequest = MSExportRequest.new();
        exportRequest.rect = abs_influence_rect;
        exportRequest.scale = export_scale_factor;
    }
    
    exportRequest.shouldTrim = false;
    
    // sketch 41 broke configureForLayer, so we have to use the new version of the API
    if (exportRequest.respondsToSelector(NSSelectorFromString("configureForLayerAncestry:layerOptions:includedIDs:"))) {
        var ancestry = [MSImmutableLayerAncestry ancestryWithMSLayer:layer_to_render];
        [exportRequest configureForLayerAncestry:ancestry layerOptions:nil includedIDs:nil];
    } else {
        exportRequest.configureForLayer(layer_to_render)
    }
    exportRequest.includeArtboardBackground = ![[layer class] isMemberOfClass:[MSArtboardGroup class]];
    [doc saveExportRequest:exportRequest toFile:path_to_file];
    
    [[[layer_to_render style] contextSettings] setOpacity:originalOpacity];
    
    if (render_in_place) {
        [background_layer removeFromParent];
        
        var ancestors = [NSMutableArray arrayWithArray: [layer_to_render ancestors]];
        [ancestors addObject: layer_to_render]; //ancestorsAndSelf did not exist in Sketch 3.5
        
        var rotation = 0;
        for (var a = [ancestors count] - 1; a > -1; a--) {
            var ancestor = [ancestors objectAtIndex: a];
            var flip = ([ancestor isFlippedHorizontal] == [ancestor isFlippedVertical]) ? 1 : -1;
            rotation = (rotation - [ancestor rotation]) * flip;
        }
        
        var image = [[NSImage alloc] initWithContentsOfFile: path_to_file];
        [[NSFileManager defaultManager] removeItemAtPath: path_to_file error: null];
        
        var image_ref = [image CGImageForProposedRect:null context:nil hints:nil];
        var original_width = CGImageGetWidth(image_ref);
        var original_height = CGImageGetHeight(image_ref);
        
        var influence_bounds = influenceRectForFrame(layer_to_render);
        var width = Math.round(influence_bounds.size.width + abs_influence_rect.size.width % 1)*export_scale_factor;
        var height = Math.round(influence_bounds.size.height + abs_influence_rect.size.height % 1)*export_scale_factor;
        
        var colorspace = CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
        var context = CGBitmapContextCreate(null, width, height,
                                            8, 4 * width, colorspace,
                                            kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big);
        CGContextSetBlendMode(context, kCGBlendModeCopy);
        CGColorSpaceRelease(colorspace);
        CGContextTranslateCTM(context, width/2, height/2);
        CGContextRotateCTM(context, rotation/180 * Math.PI);
        CGContextTranslateCTM(context, -original_width/2, -original_height/2);
        CGContextDrawImage(context, CGRectMake(0, 0, original_width, original_height), image_ref);
        var bitmap = [[NSBitmapImageRep alloc] initWithCGImage: CGBitmapContextCreateImage(context)];
        CGContextRelease(context);
        
        var image_data = [bitmap representationUsingType:NSPNGFileType properties:[[NSDictionary alloc] init]];
        [image_data writeToFile: path_to_file  atomically: true];
    } else {
        [layer_to_render removeFromParent];
    }
    
    return path_to_file;
}

function jsonObjectForBorder(border) {
    return {
    color: jsonObjectForColor([border respondsToSelector:NSSelectorFromString("color")] ? [border color] : [border colorGeneric]),
    width: [border thickness]
    };
}

function jsonObjectForColor(color) {
    return {
    r: [color red],
    g: [color green],
    b: [color blue],
    a: [color alpha]
    };
}

function getAsArray(obj) { // Sketch 39 removed the -array method form a bunch of stuff, this is a workaround
    if ([obj respondsToSelector:NSSelectorFromString("array")]) {
        obj = [obj array];
    }
    return obj;
}

function filter(array, filterFunction) {
    var result = []
    for (var i =0; i < array.count(); i++) {
        var element = array.objectAtIndex(i);
        if (filterFunction(element)) {
            result.push(element)
        }
    }
    return result;
}

function flatMap(array, mapFunction) {
    var result = []
    for (var i =0; i < array.count(); i++) {
        var element = array.objectAtIndex(i);
        result.pushNonNull(mapFunction(element))
    }
    return result;
}

Array.prototype.pushNonNull = function(obj) {if(obj !== null && obj !== undefined){this.push(obj);}}
Array.prototype.count = function() {return this.length;}
Array.prototype.objectAtIndex = function(idx) {return this[idx];};

// sketch 50 removed influenceRectForFrame
function influenceRectForFrame(layer) {
    if (layer.immutableModelObject && layer.immutableModelObject().influenceRectForFrameInDocument) {
        return [[layer immutableModelObject] influenceRectForFrameInDocument:nil];
    } else if (layer.influenceRectForFrame) {
        return layer.influenceRectForFrame();
    } else if (layer.influenceRectForBounds) {
        var originalRotation = [layer rotation];
        [layer setRotation:0];
        var boundsInfluence = layer.influenceRectForBounds();
        var frame = layer.frame();
        boundsInfluence.origin.x += [frame x];
        boundsInfluence.origin.y += [frame y];
        [layer setRotation:originalRotation];
        return boundsInfluence;
    }
    
    return layer.frame(); // fallback case
}

// Sketch 51 renamed endDecorationType & startDecorationType
function hasBorderStartEnd(style) {
    if ([style respondsToSelector:NSSelectorFromString("endMarkerType")] && [style respondsToSelector:NSSelectorFromString("startMarkerType")]) {
        return [style endMarkerType] != 0 || [style startMarkerType] != 0;
    }
    
    if ([style respondsToSelector:NSSelectorFromString("endDecorationType")] && [style respondsToSelector:NSSelectorFromString("startDecorationType")]) {
        return [style endDecorationType] != 0 || [style startDecorationType] != 0;
    }
    
    return false; // unknown, just assume it doesn't, since they usually don't
}
