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


// we declare all top level variables as var instead of
// let or const to prevent a script crash where
// sketch already declars the identifier

var sketch = require('sketch');

var has_MSImmutableLayerAncestry = NSClassFromString("MSImmutableLayerAncestry");

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,
idToMasterMap = [[NSMutableDictionary alloc] init];

// the radius syntax changed in Sketch 42 (build number '36781')
var radiusTextDivider = ";";

function recursiveLog(layer, depth) {
    let str=""
    for (let j=depth;j>0;j--) {
        str+="-";
    }
    log(str+""+[layer name]);
    
    let subs = get_sublayers(layer)
    for (let i =0; i < [subs count]; i++) {
        recursiveLog([subs objectAtIndex:i], depth+1);
    }
}

function exportDirect(exportPath, assetScale, shouldImportSelectedOnly, importIndex) { // Called Externally
    if (assetScale <= 0) {
        log("Export Scale Factor <= 0. Aborting Export");
        return;
    }
    
    export_scale_factor = assetScale;
    
    var docs = [[NSApplication sharedApplication] orderedDocuments];
    doc = [docs firstObject];
    
    if (importIndex && importIndex >= 0 && importIndex < [docs count]) {
        doc = [docs objectAtIndex: importIndex];
    }
    
    [doc showMessage:"Exporting to Principle..."];
    
    export_directory = exportPath;
    [[NSFileManager defaultManager] createDirectoryAtPath:export_directory withIntermediateDirectories:true attributes:null error:null];
    
    let layers;
    let current_page = [doc currentPage];
    let selectedLayers = [current_page selectedLayers];
    let selection = [selectedLayers isKindOfClass: [NSArray class]] ? selectedLayers : [selectedLayers layers];
    
    if (shouldImportSelectedOnly == 1) {
        let artboards = [NSHashTable hashTableWithOptions:(NSHashTableObjectPointerPersonality | NSHashTableStrongMemory)];
        for (let i = 0; i < [selection count]; i++) {
            let selected = [selection objectAtIndex:i];
            if (selected.parentArtboard) {
                [artboards addObject: [selected parentArtboard]];
            }
        }
        layers = [artboards allObjects];
    }
    
    if (!layers || [layers count] == 0) {
        layers = [current_page layers];
    }
    
    let artboards = flatMap(layers, function(layer) {
                            let canExport = [layer isMemberOfClass:[MSArtboardGroup class]] || isSymbolMaster(layer);
                            return canExport ? layer: null;
                            });
    let index = 1;
    const principleNotificationName = "com.hoopersoftware.principle.import.progressupdate";

    function artboardToMetaData(layer) {
        let includeBGColor = [layer includeBackgroundColorInExport];
        [layer setIncludeBackgroundColorInExport:false];
        [doc showMessage:"Exporting Artboard '"+[layer name]+"' to Principle..."];
        let progressInfo = {imported: index, total: [artboards count], artboardName: [layer name]};
        [[NSDistributedNotificationCenter defaultCenter] postNotificationName:principleNotificationName object:null userInfo:progressInfo deliverImmediately:true];
        index+=1;
        
        let result = process_layer(layer, "", false, false);
        
        [layer setIncludeBackgroundColorInExport:includeBGColor];
        
        return result;
    }
    
    let layers_metadata = flatMap(artboards, artboardToMetaData);
    
    // Write settings
    let settings = [[NSMutableDictionary alloc] init]; // for data.json file
    [settings setValue:export_scale_factor forKey:@"scale"];
    [settings setValue:layers_metadata forKey:@"layers"];
    
    let error = MOPointer.alloc().init()
    let scaleJSON = [NSJSONSerialization dataWithJSONObject:settings options:0 error:error];
    if (error.value) {
        log (error.value());
    }
    let settings_filepath = export_directory + "/data.json";
    [scaleJSON writeToFile: settings_filepath atomically:true];
    
    [doc showMessage:"Finished Exporting to Principle"];
}

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

function isThisACircle(layer) {
    if ([layer edited]) return false;
    
    let frame = [layer frame]
    if (Math.abs([frame width] - [frame height]) >= 1) return false;
    
    const borderCount = enabledItemsForCollection([[layer style] borders]).length;
    if (![layer isClosed] && borderCount > 0) return false;
    
    return true;
}

function isSimpleRoundedRectangle(layer) {
    
    if ([layer edited]) return -1;
    
    // it's possible for an un-edited rect to have been squished into a diamond shape via
    // parent-group resizing, we have to check that verticies are all right angles.
    
    const points = [layer points];
    if ([points count] != 4) return -1;
    
    // opening the rectangle's path marks it as edited, so this check is redundant
    //    const borderCount = enabledItemsForCollection([[layer style] borders]).length;
    //    if (![layer isClosed] && borderCount > 0) return -1;
    
    function isRightAngle(vertex, b, c) {
        let vbx = (b.x - vertex.x);
        let vby = (b.y - vertex.y);
        let vcx = (c.x - vertex.x);
        let vcy = (c.y - vertex.y);
        return Math.abs(vbx*vcx + vby*vcy) < 0.001;
    }
    
    const firstCurvePoint = [points objectAtIndex:0];
    if (![firstCurvePoint isEffectivelyStraight]) return -1;
    
    const radius = [firstCurvePoint cornerRadius];
    
    const first = [firstCurvePoint point];
    let second = [[points objectAtIndex:1] point];
    let third = [[points objectAtIndex:2] point];
    let far = [[points objectAtIndex:3] point];
    
    // sort points so they're like so
    // first ------- 2/3
    //   |            |
    //  2/3 -------- far
    {
        let furthestIndex = 0;
        let distance = -1;
        for (let i=1;i < 4;i++) {
            const p = [points objectAtIndex:i];
            if (![p isEffectivelyStraight]) return -1;
            if (Math.abs(radius - [p cornerRadius]) > 0.0001) return -1;
            
            const point = [p point];
            const dx = point.x-first.x;
            const dy = point.y-first.y;
            const dist = (dx*dx+dy*dy);
            
            if (dist > distance) {
                distance = dist;
                furthestIndex = i;
            }
        }
        
        if (furthestIndex == 1) {
            const tmp = second;
            second = far;
            far = tmp;
        } else if (furthestIndex == 2) {
            const tmp = third;
            third = far;
            far = tmp;
        }
    }
    
    // check that the points form right angles
    if (!isRightAngle(first, second, third)) return -1;
    if (!isRightAngle(second, far, first)) return -1;
    if (!isRightAngle(far, second, third)) return -1;
    
    return radius;
}

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]]
        && ![layer isKindOfClass:[MSShapePathLayer class]]
        && !isSymbolMaster(layer)
        && !isSymbolInstance(layer)
        && !isTextLayer(layer)) {
        return;
    }
    
    let shouldIgnoreLayer = (([layer name].toLowerCase().replace(/\s+/g, '').indexOf("principleskip") != -1) || ![layer isVisible]);
    if (shouldIgnoreLayer) { return; }
    
    let remove_after_processed = false;
    let originalLayer = layer;
    
    if (isSymbolInstance(layer)) {
        layer = detachSymbolTree(layer, null, null);
        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)) {
        let immutable = [[MSImmutableTextLayer alloc] init];
        [immutable performInitWithMutableModelObject: layer];
        if ([immutable textContainerSize].width < influenceRectForFrame(layer).size.width ) {
            [layer adjustFrameToFit];
        }
    }
    
    let imagePath, layers_holder, sub, newUUIDStack = uuidStack;
    
    let style = [layer style];
    
    let tmpName = [layer name].replace(/principle\s*flatten/gi, '');
    let name_requests_flattening = (tmpName != [layer name]);
    tmpName =  tmpName.replace(/@[0123456789]+x/g,"").trim();
    
    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 = "";
    }
    
    let layer_data = {
    name: tmpName,
    opacity: [[style contextSettings] opacity],
    };
    
    if ([layer isLocked]) {
        layer_data.locked = true;
    }
    
    let sublayers = get_sublayers(layer);
    
    layer_data["id"] = (uuidStack + [layer objectID]);
    let layer_master = [idToMasterMap objectForKey: [layer objectID]];
    if (layer_master) {
        newUUIDStack = layer_data["id"] + "-";
    }

    function calculateRasterStyle() {
        // check for reasons to rasterize all layer properties //

        if (name_requests_flattening) {
            layer_data.flatten_reason = "The layer name in Sketch contains 'principle flatten'";
            return RasterizeStyle.All; // layer name requests flattening
        }
        
        let blur = [style blur];
        if (blur && [blur isEnabled]) {
            layer_data.flatten_reason = "The Sketch layer has blur effect";
            return RasterizeStyle.All; // blur enabled
        }
        
        for (let j = 0; j < [sublayers count]; j++) {
            if ([[sublayers objectAtIndex:j] isMasked]) {
                layer_data.flatten_reason = "The Sketch group contains a mask";
                return RasterizeStyle.All; // flatten groups containing a mask
            }
        }
        
        let fillColor = null, radius = null;

        const enabledFills = enabledItemsForCollection([style fills]);
        
        if ([layer isMemberOfClass:[MSLayerGroup class]] && !layer_master) {
            if (enabledFills.length > 0) {
                layer_data.flatten_reason = "The Group has a tint color";
                return RasterizeStyle.All;
            }

        } else if (layer_master) {
            if ([layer_master includeBackgroundColorInInstance] && [layer_master hasBackgroundColor]) {
                fillColor = jsonObjectForColor([layer_master backgroundColor]);
            } else {
                fillColor = {r:1,g:1,b:1,a:0};
            }
        } else if ([layer isMemberOfClass:[MSArtboardGroup class]] || isSymbolMaster(layer)) {
            fillColor = [layer hasBackgroundColor] ? jsonObjectForColor([layer backgroundColor]) : {r:1,g:1,b:1, a:1};
        } else if ([layer isMemberOfClass:[MSOvalShape class]]) {
            if (!isThisACircle(layer)) return RasterizeStyle.All;
            radius = [[layer frame] width]/2;
        } else if ([layer isMemberOfClass:[MSRectangleShape class]]) {
            let tmpradius = isSimpleRoundedRectangle(layer);
            if (tmpradius < 0) return RasterizeStyle.All;
            radius = tmpradius;
        } else if ([layer isMemberOfClass:[MSTextLayer class]]) {
            layer_data.flatten_reason = "Text layers are always flattened so they match Sketch exactly";
            return RasterizeStyle.All;
        } else {
            return RasterizeStyle.All; // rasterize all other layer classes
        }
        
        let enabledShadows = enabledItemsForCollection([style shadows]);
        if (enabledShadows.length > 1) {
            layer_data.flatten_reason = "The Sketch layer has multiple shadows";
            return RasterizeStyle.All; // multiple shadows
        }
        
        if (enabledShadows.length == 1) {
            let shadowInfo = enabledShadows[0];
            if ([shadowInfo spread] != 0) {
                layer_data.flatten_reason = "The Sketch layer has a shadow spread greater than 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 //
        
        let enabledBorders = enabledItemsForCollection([style borders]);
        if (enabledBorders.length > 1) {
            layer_data.flatten_reason = "The Sketch layer has multiple borders";
            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 ([style endMarkerType] != 0 || [style startMarkerType] != 0 || [[style borderOptions] hasDashPattern]) {
                layer_data.flatten_reason = "The Sketch layer has a stylized border";
                return RasterizeStyle.AllExceptShadow; // has arrow or border is dashed
            }
            
            if (enabledBorders.length == 1) {
                let firstBorder = enabledBorders[0];
                if ([firstBorder position] != 1) {
//                    layer_data.flatten_reason = "Layer's border style is not inside";
                    return RasterizeStyle.AllExceptShadow; // has border that is not inside
                }

                if ([firstBorder fillType] != 0) {
                    return RasterizeStyle.AllExceptShadow; // has border that is not solid
                }
                layer_data.border = {
                    color: jsonObjectForColor([firstBorder color]),
                    width: [firstBorder thickness]};
            }
        }
        
        // check for reasons to rasterize fill //
        
        if (enabledItemsForCollection([style innerShadows]).length > 0) {
              return RasterizeStyle.AllExceptShadowAndBorder; // has inner shadows
        }
        

        if (enabledFills.length > 1) {
             return RasterizeStyle.AllExceptShadowAndBorder; // multiple fills
        }
        
        if (enabledFills.length == 1 && [layer isKindOfClass:[MSShapePathLayer class]]) {
            let firstFill = enabledFills[0];
            if ([firstFill fillType] != 0) {
                return RasterizeStyle.AllExceptShadowAndBorder; // rasterizing because of fill type
            }
            
            fillColor = jsonObjectForColor([firstFill color]);
        }
        
        if (fillColor) {
            layer_data.fillColor = fillColor;
        }
        
        if (radius) {
            layer_data.radius = radius;
        }
        
        return RasterizeStyle.None;
    }
    let rasterize = calculateRasterStyle();
    
    let is_flipped_horizontal = parent_flipped_horizontal != [layer isFlippedHorizontal];
    let is_flipped_vertical = parent_flipped_vertical != [layer isFlippedVertical];


    
    if (rasterize) {
        layer_data.fillColor = {r:1,g:1,b:1,a:0};
        
        let itemsToEnable = [];
        function disableItems(items) {
            for (let i = 0; i < [items count]; i++) {
                let item = [items 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
        let originalOpacity = [[[layer style] contextSettings] opacity];
        let originalRotation = [layer rotation];
        [[[layer style] contextSettings] setOpacity:1];
        [layer setRotation:0];


        const influenceFrame = influenceRectForFrame(layer);

        { // calculate new layer origin
            const unrotated_frame = [layer frame];

            const frame_center = {
            x: [unrotated_frame x] + [unrotated_frame width]/2,
            y: [unrotated_frame y] + [unrotated_frame height]/2
            };

            const influence_frame_center = {
            x: influenceFrame.origin.x + influenceFrame.size.width/2,
            y: influenceFrame.origin.y + influenceFrame.size.height/2
            };

            const influence_delta = {
            x:influence_frame_center.x - frame_center.x,
            y:influence_frame_center.y - frame_center.y
            };

            const radians_angle = originalRotation / 180 * 3.14159265;

            const rotated_delta = {
                x:Math.sin(radians_angle)*influence_delta.y + Math.cos(radians_angle)*influence_delta.x,
                y:Math.cos(radians_angle)*influence_delta.y - Math.sin(radians_angle)*influence_delta.x
            };

            layer_data.x = frame_center.x + rotated_delta.x - influenceFrame.size.width/2;
            layer_data.y = frame_center.y + rotated_delta.y - influenceFrame.size.height/2;
        }

        [layer setRotation:originalRotation];
        [[[layer style] contextSettings] setOpacity:originalOpacity];
        
        layer_data.image = export_layer(layer, is_flipped_horizontal, is_flipped_vertical);
        layer_data.w = influenceFrame.size.width;
        layer_data.h = influenceFrame.size.height;
        
        for (let i = 0; i < [itemsToEnable count]; i++) {
            let theItemToEnable = itemsToEnable[i];
            [theItemToEnable setIsEnabled:true];
        }
    } else {
        const frame = [layer frame];
        layer_data.x = [frame x];
        layer_data.y = [frame y];
        layer_data.w = [frame width];
        layer_data.h = [frame height];
        
        layers_holder = [];
        
         for (let 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];
        }
    }
    
    let 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) {
        if ([layer isMemberOfClass:[MSLayerGroup class]] || [layer isMemberOfClass:[MSArtboardGroup class]] || isSymbolMaster(layer)) {
            return [layer layers];
        } else if (isSymbolInstance(layer)) {
            return [[layer symbolMaster] layers];
        }
    }
    
    return [];
}

function detachSymbolTree(layer, overrides, uuidpath) {
    let override = null;
    
    let localPath = (uuidpath ? (uuidpath+"/") : "")+[layer objectID];
    
    if (isSymbolInstance(layer)) {
        let master = [layer symbolMaster];
        if (overrides) {
            override = overrides.find(function(e){return e.path == localPath && e.property === "symbolID";}) || null;
        }
        if (override) {
            if (override.value != "") {
                master = [[doc documentData] symbolWithID: override.value];
            } else {
                master = null;
            }
        }
        
        if (master == null) { return null; }
        
        [idToMasterMap setObject:master forKey:[layer objectID]];
        
        let 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]];
        
        let originalParent = [layer parentGroup];
        [originalParent insertLayer:newGroup atIndex:[originalParent indexOfLayer:layer]];
        
        //add sublayers
        let sublayers = [master layers];
        
        for (let k = 0; k < [sublayers count]; k++) {
            let originalSublayer = sublayers[k];
            let newSublayer = [originalSublayer copy];
            
            [newGroup addLayer:newSublayer];
            
            function copyIDsFromLayerTree(source_layer, destination_layer) {
                destination_layer.objectID = [source_layer objectID];
                
                if (isSymbolInstance(destination_layer)) {
                    return;
                }
                
                let destination_sublayers = get_sublayers(destination_layer);
                let source_sublayers = get_sublayers(source_layer);
                let sublayer_count = Math.min([destination_sublayers count], [source_sublayers count]);
                
                for (let s = 0; s < sublayer_count; s++) {
                    copyIDsFromLayerTree(source_sublayers[s], destination_sublayers[s]);
                }
            }
            
            copyIDsFromLayerTree(originalSublayer, newSublayer);
        }
        
        //resize layers
        let old_size = CGSizeMake([[master frame] width], [[master frame] height]);
        [newGroup resizeChildrenWithOldSize:old_size];
        
        if (overrides === null) {
            overrides = sketch.fromNative(layer).overrides || [];
        } else {
            uuidpath = localPath;
            [layer removeFromParent];
        }
        
        layer = newGroup;
        
    } else if (overrides) {
        let tmplayer = sketch.fromNative(layer);
        let document = sketch.fromNative(doc);
        
        for (let i = 0; i < overrides.length; i++) {
            override = overrides[i];
            if (override.path != localPath) { continue; }
            
            if (override.property === "stringValue" && isTextLayer(layer)) {
                tmplayer.text = override.value;
            } else if (override.property === "image") {
                if ([layer isMemberOfClass: [MSBitmapLayer class]]) {
                    tmplayer.image = override.value;
                } else if ([layer isMemberOfClass:[MSRectangleShape class]]) {
                    let enabledFills = enabledItemsForCollection([[layer style] fills]);
                    if (override.value) {
                        for (let p = ([enabledFills count] - 1); p >= 0; p--) {
                            let fill = enabledFills[p];
                            if ([fill fillType] === 4) {
                                fill.image = override.value.sketchObject;
                                break;
                            }
                        }
                    }
                }
            } else if (override.property === "layerStyle") {
                let sharedStyle = document.getSharedLayerStyleWithID(override.value);
                if (sharedStyle) {
                    tmplayer.style = sharedStyle.style;
                }
            } else if (override.property === "textStyle") {
                let sharedStyle = document.getSharedTextStyleWithID(override.value);
                if (sharedStyle) {
                    tmplayer.style = sharedStyle.style;
                }
            } else {
                log("unhandled property: "+override.property);
            }
        }
    }
    
    //detach sublayers, remove layers that cannot be detached
    let sublayers = get_sublayers(layer);
    for (let s = 0; s < [sublayers count]; s++) {
        let sublayer_el = sublayers[s];
        let detachedLayer = detachSymbolTree(sublayer_el, overrides, uuidpath);
        if (!detachedLayer) {
            [sublayer_el removeFromParent];
            s--;
        }
    }
    
    return layer;
}



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

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

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

function LayerAbsoluteInfluenceRectShim(layer) {

    if (layer.absoluteInfluenceRect) {
        // Sketch < 96
        return layer.absoluteInfluenceRect();
    }

    const immutable = layer.immutableModelObject();
    if (!immutable || !immutable.influenceRectForBoundsInDocument) {
        return [layer absoluteBoundingBox];
    }

    // Sketch >= 96
    const document = layer.documentData();
    const relativeInfluenceRect = immutable.influenceRectForBoundsInDocument(document);
    return layer.convertRect_toLayer_(relativeInfluenceRect, /* to absolute/page coordinates */null)
}

function export_layer(layer, is_flipped_horizontal, is_flipped_vertical) {
    let path_to_file = export_directory + "/assets/" + assetNumber + ".png";
    assetNumber++;
    
    let layer_to_render = layer;
    
    let style = [layer style];
    let blur_nullable = [style blur];
    let render_in_place = (blur_nullable && [blur_nullable type] > 2 && [blur_nullable isEnabled]);
    
    let artboard = [layer_to_render parentArtboard];
    let background_layer = null;
    if (render_in_place) {
        let frame = [artboard frame];
        background_layer = new sketch.Shape({
                                    parent: sketch.fromNative(artboard),
                                    frame: new sketch.Rectangle(0,0,[frame width],[frame height])
                                    });
        background_layer.style.fills = [{
                                color: '#ffffffff',
                                fill: sketch.Style.FillType.Color,
                                }];
        background_layer.moveToBack();
        let style = background_layer.style.sketchObject;
        let backgroundFills = [style fills];
        backgroundFills = backgroundFills[0];
        let color = [artboard hasBackgroundColor] ? [artboard backgroundColor] : [MSColor colorWithNSColor: [NSColor whiteColor]];
        backgroundFills.color = color;
    } 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];
        
        let 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

    let originalOpacity = [[[layer_to_render style] contextSettings] opacity];
    [[[layer_to_render style] contextSettings] setOpacity:1];

    let abs_influence_rect = LayerAbsoluteInfluenceRectShim(layer_to_render);
    
    let exportRequest = MSExportRequest.new();
    exportRequest.rect = abs_influence_rect;
    exportRequest.scale = export_scale_factor;
    exportRequest.shouldTrim = false;

//    const options = { scales: ''+export_scale_factor, formats: 'png',output:"~/Desktop", trimmed:false };
//    sketch.export(layer_to_render, options);

    if(has_MSImmutableLayerAncestry) {
        // sketch 66 removed MSImmutableLayerAncestry, causing this code to crash.
        let ancestry = [MSImmutableLayerAncestry ancestryWithMSLayer:layer_to_render];
        [exportRequest configureForLayerAncestry:ancestry layerOptions:nil includedIDs:nil];
    } else {
        [exportRequest configureForLayerAncestry:layer_to_render.ancestry() layerOptions:nil includedIDs:nil];
    }

    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.remove();
        
        let ancestors = [NSMutableArray arrayWithArray: [layer_to_render ancestors]];
        [ancestors addObject: layer_to_render]; //ancestorsAndSelf did not exist in Sketch 3.5

        let rotation = 0;
        for (let a = [ancestors count] - 1; a > -1; a--) {
            let ancestor = [ancestors objectAtIndex: a];
            let flip = ([ancestor isFlippedHorizontal] == [ancestor isFlippedVertical]) ? 1 : -1;
            rotation = (rotation - [ancestor rotation]) * flip;
        }
        
        let image = [[NSImage alloc] initWithContentsOfFile: path_to_file];
        [[NSFileManager defaultManager] removeItemAtPath: path_to_file error: null];
        
        let image_ref = [image CGImageForProposedRect:null context:nil hints:nil];
        let original_width = CGImageGetWidth(image_ref);
        let original_height = CGImageGetHeight(image_ref);
        let influence_bounds = influenceRectForFrame(layer_to_render);
        let width = Math.round(influence_bounds.size.width + abs_influence_rect.size.width % 1)*export_scale_factor;
        let height = Math.round(influence_bounds.size.height + abs_influence_rect.size.height % 1)*export_scale_factor;
        
        let colorspace = CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
        let 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);
        let bitmap = [[NSBitmapImageRep alloc] initWithCGImage: CGBitmapContextCreateImage(context)];
        CGContextRelease(context);
        
        let 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 jsonObjectForColor(color) {
    return {
    r: [color red],
    g: [color green],
    b: [color blue],
    a: [color alpha]
    };
}

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

function flatMap(array, mapFunction) {
    let result = []
    for (let i =0; i < array.count(); i++) {
        let 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) {
    let result;
    if (layer.immutableModelObject && layer.immutableModelObject().influenceRectForBoundsInDocument) {
        const imo = [layer immutableModelObject];
        const influenceBounds = [imo influenceRectForBoundsInDocument:nil];
        result = [imo transformRectToParentCoordinates:influenceBounds];
    }else if (layer.immutableModelObject && layer.immutableModelObject().influenceRectForFrameInDocument) {
        result = [[layer immutableModelObject] influenceRectForFrameInDocument:nil];
    } else if (layer.influenceRectForFrame) {
        result = layer.influenceRectForFrame();
    } else if (layer.influenceRectForBounds) {
        let originalRotation = [layer rotation];
        [layer setRotation:0];
        let boundsInfluence = layer.influenceRectForBounds();
        let frame = layer.frame();
        boundsInfluence.origin.x += [frame x];
        boundsInfluence.origin.y += [frame y];
        [layer setRotation:originalRotation];
        result = boundsInfluence;
    } else {
        result = layer.frame(); // fallback case
    }

    // sketch 73 started returning a MSRect Object instead of a
    // CGRect struct for some of the above codepaths
    if (typeof result.size === "undefined") {
        result = {
        size:{
        width:[result width],
        height:[result height]
        },
        origin:{
        x:[result x],
        y:[result y]
        }
        };
    }

    return result;
}

