"use strict";
log(`principle: add ${2-principle_getCurrentLine()} to line numbers`);
 
//              Danger. Turn back.
//
//                   ______
//                .-"      "-.
//               /            \
//   _          |              |          _
//  ( \         |,  .-.  .-.  ,|         / )
//   > "=._     | )(__/  \__)( |     _.=" <
//  (_/"=._"=._ |/     /\     \| _.="_.="\_)
//         "=._ (_     ^^     _)"_.="
//             "=\__|IIIIII|__/="
//            _.="| \IIIIII/ |"=._
//  _     _.="_.="\          /"=._"=._     _
// ( \_.="_.="     `--------`     "=._"=._/ )
//  > _.="                            "=._ <
// (_/                                    \_)
//

function principle_getCurrentLine(stack_shift) {
    try {
        throw new Error();
    } catch (e) {
        const stackLines = e.stack.split('\n');
        const line = stackLines[1+(stack_shift || 0)].trim();
        const lineNumber = parseInt(line.substring(line.indexOf(':') + 1));
        return lineNumber;
    }
}

const exportDirect = (function() {
    
    const sketchLogNotification = "com.hoopersoftware.principle.import.log";
    
    function debug_log_to_principle(str) {
        const payload = {data:str};
        [[NSDistributedNotificationCenter defaultCenter] postNotificationName:sketchLogNotification object:null userInfo:payload deliverImmediately:true];
    }
    
    function print(m) {
        log("principle: " + m);
//        debug_log_to_principle(m);
    };
    
    function assert(condition, message) {
        if (!condition) {
            throw new Error(message || "Assertion failed");
        }
    }

    const sketch = require('sketch');

    const RasterizeStyle = Object.freeze({
        None: 0,
        Fill: 1,
        Fill_Border: 2,
        fill_border_shadow: 3,
        fill_border_shadow_sublayers: 4,
    });

    const BorderPosition = Object.freeze({
        CENTER:0,
        INSIDE:1,
        OUTSIDE:2
    });

    const BlurType = Object.freeze({
        BACKGROUND:3,
    });

    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];};

    let doc, export_directory;
    let export_scale_factor = 1;
    let assetNumber = 1;

    function dump(obj){
        print("######################################")
        print("## Dumping object " + obj )
        print("######################################")

        function logEach(a)  {
            for (let i=0;i < a.length; i++) {
                print(a[i]);
            }
        }

        if (typeof obj == 'undefined'){
            print("obj is undefined");
        } else if (obj === null) {
            print("obj is null");
        } else if (typeof obj.className === 'function') {
            print("## obj class is: " + [obj className]);

            print("obj class hierarchy:")
            let theclass = [obj class];

            while (theclass != nil) {
                print(theclass);
                theclass = [theclass superclass];
            }

            print("obj.properties:");
            logEach([obj class].mocha().properties());
            print("obj.propertiesWithAncestors:");
            logEach([obj class].mocha().propertiesWithAncestors());

            print("obj.classMethods:");
            logEach([obj class].mocha().classMethods());
            print("obj.classMethodsWithAncestors:");
            logEach([obj class].mocha().classMethodsWithAncestors());

            print("obj.instanceMethods:");
            logEach([obj class].mocha().instanceMethods());
            print("obj.instanceMethodsWithAncestors:");
            logEach([obj class].mocha().instanceMethodsWithAncestors());

            print("obj.protocols:");
            logEach([obj class].mocha().protocols());
            print("obj.protocolsWithAncestors:");
            logEach([obj class].mocha().protocolsWithAncestors());

            print("obj.treeAsDictionary():");
            logEach(obj.treeAsDictionary());
        } else {
            print("## obj is does not have className function, printing javascript fields:");
            for(let propName in obj) {
                print("obj."+propName+": "+obj[propName]);
            }
        }
    }

    function recursiveLog(layer, depth) {
        let str=""
        for (let j=depth;j>0;j--) {
            str+="-";
        }
        print(str+""+[layer name]);

        let subs = get_sublayers(layer)
        for (let i =0; i < [subs count]; i++) {
            recursiveLog([subs objectAtIndex:i], depth+1);
        }
    }

    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_js, uuidStack, parent_flipped_horizontal, parent_flipped_vertical) {
        
        
        if (layer_js.type !== "Group" &&
            layer_js.type !== "Artboard" &&
            layer_js.type !== "SymbolMaster" &&
            layer_js.type !== "SymbolInstance" &&
            layer_js.type !== "Text" &&
            layer_js.type !== "Image" &&
            layer_js.type !== "ShapePath" &&
            layer_js.type !== "Shape") {
            return;
        }

        const shouldIgnoreLayer = ((layer_js.name.toLowerCase().replace(/\s+/g, '').indexOf("principleskip") != -1) || layer_js.hidden);
        if (shouldIgnoreLayer) { return; }

        let remove_after_processed = false;
        let layer = layer_js.sketchObject;
        const originalLayer = layer;

        if (layer_js.type === "SymbolInstance") {
            // we used to detach symbols ourselves in order to maintain stable uuids across imports, but
            // with sketch's symbols getting more complicated, it became error prone to
            // detach ourselves, so now we just call sketch's detach function
            const duplicate_layer = layer_js.duplicate();
            layer_js = duplicate_layer.detach({ recursively: true})
            duplicate_layer.remove();

            if (!layer_js) {
                return;
            }
            
            layer = layer_js.sketchObject;

            remove_after_processed = true;
            [originalLayer setIsVisible: false];
        }

        if (!layer) {
            return;
        }
        
        // workaround for sketch 39 making fixed width layers really wide.
        if (layer_js.type === "Text") {
            let immutable = [[MSImmutableTextLayer alloc] init];
            [immutable performInitWithMutableModelObject: layer];
            if ([immutable textContainerSize].width < influenceRect(layer_js).size.width ) {
                [layer adjustFrameToFit];
            }
        }

        let imagePath, sub, newUUIDStack = uuidStack;

        let style = [layer style];
        
        // FYI: I have no idea what the difference is between fills, regularFills, and raw_fills.
        const enabledFills = enabledItemsForCollection([style regularFills]);
        if ((layer_js.type === "Group" || layer_js.type === "Artboard" || layer_js.type == "SymbolMaster") &&
        ([enabledFills count] == 0 && layer_js.layers.length == 0)) {
            return;
        }

        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_js.locked) {
            layer_data.locked = true;
        }
        
        let sublayers = get_sublayers(layer);

        layer_data["id"] = (uuidStack + [layer objectID]);

        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.fill_border_shadow_sublayers; // layer name requests flattening
            }

            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.fill_border_shadow_sublayers; // flatten groups containing a mask
                }
            }
            
            // Sketch 102 introduced the blurs array
            let blurs = style.blurs?.() || [];
            let has_background_blur = false;
            for (let i=0; i < blurs.length || 0; i++) {
                let blur = blurs[i];
                if (blur.isEnabled?.()) {
                    layer_data.flatten_reason = "The Sketch layer has a blur effect";
                    if ([blur type] == BlurType.BACKGROUND) {
                        has_background_blur = true;
                    } else {
                        return RasterizeStyle.fill_border_shadow_sublayers;
                    }
                }
            }
            
            // we have this return here because a non background blur takes
            // priority over background blur because those requires blurring the sublayers
            if (has_background_blur) {
                return RasterizeStyle.fill_border_shadow;
            }

            let fillColor = null, radius = null;

            if (layer_js.type === "Shape") {
                return RasterizeStyle.fill_border_shadow_sublayers;
            } else if (layer_js.type === "Group" || layer_js.type === "Artboard" || layer_js.type == "SymbolMaster") {
                if (layer.firstTint?.()?.isEnabled()) {
                    layer_data.flatten_reason = "The Layer has a tint color";
                    return RasterizeStyle.fill_border_shadow_sublayers;
                }
                if (layer_js.type === "Artboard" || layer_js.type == "SymbolMaster") {
                    fillColor = (layer.hasBackgroundColor?.() && layer.backgroundColor) ? jsonObjectForColor([layer backgroundColor]) : {r:1,g:1,b:1, a:1};
                }
            } else if (layer_js.type == "SymbolInstance") {
                // TODO: handle background colors of instances
//                if (layer_master.includeBackgroundColorInInstance?.() && layer_master.hasBackgroundColor?.() && layer_master.backgroundColor) {
//                    fillColor = jsonObjectForColor([layer_master backgroundColor]);
//                } else {
//                    fillColor = {r:1,g:1,b:1,a:0};
//                }
            } else if (layer_js.type == "ShapePath" && layer_js.shapeType == "Oval") {
                if (!isThisACircle(layer)) return RasterizeStyle.fill_border_shadow_sublayers;
                radius = [[layer frame] width]/2;
            } else if (layer_js.type == "ShapePath" && layer_js.shapeType == "Rectangle") {
                let tmpradius = isSimpleRoundedRectangle(layer);
                if (tmpradius < 0) return RasterizeStyle.fill_border_shadow_sublayers;
                radius = tmpradius;
            } else if (layer_js.type == "Text") {
                layer_data.flatten_reason = "Text layers are always flattened so they match Sketch exactly";
                return RasterizeStyle.fill_border_shadow_sublayers;
            } else {
                return RasterizeStyle.fill_border_shadow_sublayers; // rasterize all other layer classes
            }

            var enabled_shadows=filter(layer_js.style.shadows, s=>s.enabled);
            if (enabled_shadows.length > 1) {
                layer_data.flatten_reason = "The Sketch layer has multiple shadows";
                return RasterizeStyle.fill_border_shadow; // multiple shadows
            } else if (enabled_shadows.length == 1) {
                let shadowInfo = enabled_shadows[0];
                if (shadowInfo.spread != 0) {
                    layer_data.flatten_reason = "The Sketch layer has a shadow spread greater than 0";
                    return RasterizeStyle.fill_border_shadow; // shadow spread > 0
                }

                layer_data.shadow = {
                    color: jsonObjectForColor(shadowInfo.sketchObject.color()),
                    x:shadowInfo.x,
                    y:shadowInfo.y,
                    blur:shadowInfo.blur
                };
            }
            
            // 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.Fill_Border; // multiple borders
            }

            // MSLayerGroup check for Sketch bug: some groups have an invisible border
            if (enabledBorders.length != 0 && ![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.Fill_Border; // has arrow or border is dashed
                }

                if (enabledBorders.length == 1) {
                    let firstBorder = enabledBorders[0];
                    if ([firstBorder position] != BorderPosition.INSIDE) {
                        layer_data.flatten_reason = "The border style is not 'inside'";
                        return RasterizeStyle.Fill_Border; // has border that is not inside
                    }

                    if ([firstBorder fillType] != 0) {
                        layer_data.flatten_reason = "The border fill is not solid";
                        return RasterizeStyle.Fill_Border; // has border that is not solid
                    }
                    layer_data.border = {
                    color: jsonObjectForColor([firstBorder color]),
                        width: [firstBorder thickness]};
                }
            }
            
            // save the radius after confirming we won't rastarize the border
            if (radius) {
                layer_data.radius = radius;
            }

            //
            // check for reasons to rasterize fill
            //
            let enabled_inner_shadows = filter(layer_js.style.innerShadows, s=>s.enabled);
            if (enabled_inner_shadows.length > 0) {
                layer_data.flatten_reason = "Layer has inner shadow";
                return RasterizeStyle.Fill; // has inner shadows
            }

            if (enabledFills.length > 1) {
                return RasterizeStyle.Fill; // multiple fills
            } else if (enabledFills.length == 1) {
                let firstFill = enabledFills[0];
                if ([firstFill fillType] != 0) {
                    return RasterizeStyle.Fill; // rasterizing because of fill type
                }

                fillColor = jsonObjectForColor([firstFill color]);
            }

            if (fillColor) {
                layer_data.fillColor = fillColor;
            }

            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 != RasterizeStyle.None) {
            layer_data.fillColor = {r:1,g:1,b:1,a:0};
            
            let jsItemsToEnable = [];
            function disableJSItems(items) {
                for (let item of items) {
                    if (item.enabled) {
                        jsItemsToEnable.push(item);
                        item.enabled = false;
                    }
                }
            }
            
            let layers_to_set_visible = [];
            if (rasterize != RasterizeStyle.fill_border_shadow_sublayers && layer_js.layers?.length > 0) {
                for (let sublayer of layer_js.layers) {
                    if (!sublayer.hidden) {
                        layers_to_set_visible.push(sublayer);
                        sublayer.hidden = true;
                    }
                }
            }
            
            if (rasterize < RasterizeStyle.Fill_Border) {
                disableJSItems(layer_js.style.borders);
            }

            if (rasterize < RasterizeStyle.fill_border_shadow) {
                disableJSItems(layer_js.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 originally_included_background_color = layer.includeBackgroundColorInExport?.();
            layer.setIncludeBackgroundColorInExport?.(true);
            

            const influenceFrame = influenceRect(layer_js);

            { // 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_js, is_flipped_horizontal, is_flipped_vertical);
            layer_data.w = influenceFrame.size.width;
            layer_data.h = influenceFrame.size.height;
            
            for (let item of jsItemsToEnable) {
                item.enabled = true;
            }
            
            layer.setIncludeBackgroundColorInExport?.(originally_included_background_color);
            
            for (let sublayer of layers_to_set_visible) {
                sublayer.hidden = false;
            }
        } 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];
        }
        
        
        if (rasterize != RasterizeStyle.fill_border_shadow_sublayers && layer_js.layers) {
            let sublayer_info = [];
            for (let sublayer of layer_js.layers) {
                sublayer_info.pushNonNull(process_layer(sublayer, newUUIDStack, is_flipped_horizontal, is_flipped_vertical))
            }

            if (sublayer_info.length > 0) {
                layer_data.layers = sublayer_info;
            }
        }

        if (Math.abs([layer rotation]) > 0.01) {
            if (is_flipped_horizontal == is_flipped_vertical) {
                layer_data.angle = -[layer rotation];
            } else {
                layer_data.angle = [layer rotation];
            }
        }

        const 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]] || isSymbolMaster(layer)) {
                return [[layer layers] copy];
            } else if (isSymbolInstance(layer)) {
                return [[[layer symbolMaster] layers] copy];
            }
        }

        return [];
    }

    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 CGRectToString(rect) {
        return `${rect.origin.x},${rect.origin.y} ${rect.size.width}x${rect.size.height}`
    }

    function export_layer(layer_js, is_flipped_horizontal, is_flipped_vertical) {
        const duplicate_layer_js = layer_js.duplicate();
        const layer_to_render = duplicate_layer_js.sketchObject;
        const assets_path = export_directory + "/assets/";
        const path_to_file = assets_path + assetNumber + ".png";
        
//        const blur_nullable = [[layer style] blur];
//        const render_in_place = (blur_nullable && [blur_nullable type] == BlurType.BACKGROUND && [blur_nullable isEnabled]);
//        if (render_in_place) {
//            print("rendering in place"+layer.name);
//            layer_to_render = layer;
//        } else {
            
            [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
        [[[layer_to_render style] contextSettings] setOpacity:1];
        const export_rect = influenceRect(sketch.fromNative(layer_to_render));
        let s = new sketch.Slice({
            name: ""+assetNumber,
            parent: sketch.fromNative([doc currentPage]),
            frame:  {
                x: export_rect.origin.x,
                y: export_rect.origin.y,
                width: export_rect.size.width,
                height: export_rect.size.height
            }
        });
    
        sketch.export(s, {
            formats:"png",
            output: assets_path,
            scale:export_scale_factor,
            trimmed: false,
            overwriting: true,
        });
        s.remove()
        duplicate_layer_js.remove();
        assetNumber++;
        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;
    }

    function CGRectFromMSRect(msrect) {
        return CGRectMake(msrect.x(),msrect.y(), msrect.width(),msrect.height());
    }

    function influenceRect(jslayer) {
        const layer = jslayer.sketchObject;
        let frame = CGRectFromMSRect(layer.frame());

        // expand for border
        let border_outset = 0;
        for (let border of jslayer.style.borders) {
            if (!border.enabled) continue;
            if (border.position == "Outside") {
                border_outset = Math.max(border_outset, border.thickness);
            } else if (border.position == "Center") {
                border_outset = Math.max(border_outset, border.thickness/2);
            }
        }

        // expand for shadow
        let left = 0;
        let right = 0;
        let up = 0;
        let down = 0;
        for (let shadow of jslayer.style.shadows) {
            if (!shadow.enabled) continue;
            const expansion = shadow.spread + shadow.blur;
            left    = Math.min(left,    shadow.x-expansion);
            right   = Math.max(right,   shadow.x+expansion);
            up      = Math.min(up,      shadow.y-expansion);
            down    = Math.max(down,    shadow.y+expansion);
        }


        //
        // apply expansion
        //

        left    -= border_outset;
        right   += border_outset;
        up      -= border_outset;
        down    += border_outset;

        frame.origin.x += left;
        frame.origin.y += up;
        frame.size.width += right-left;
        frame.size.height += down-up;

        // expand for blur
        
        // Sketch 102 introduced the blurs array
        let max_blur_expansion = 0;
        let blurs = layer.style().blurs?.() || [];
        for (let i=0; i < blurs.length; i++) {
            let blur = blurs[i];
            if (blur.isEnabled?.() && [blur type] != BlurType.BACKGROUND) {
                const blur_expansion = [blur radius]*3;
                if (max_blur_expansion < blur_expansion) max_blur_expansion = blur_expansion;
            }
        }
        
        frame = CGRectInset(frame,-max_blur_expansion,-max_blur_expansion);
        
        return CGRectIntegral(frame);//CGRectInset(,-1,-1); // expand by 1 pixel for antialiasing
    }

    return function(exportPath, assetScale, shouldImportSelectedOnly, importIndex) { // Called Externally
        if (assetScale <= 0) {
            print("Export Scale Factor <= 0. Aborting Export");
            return;
        }

        export_scale_factor = assetScale;

        const native_docs = [[NSApplication sharedApplication] orderedDocuments];
        if (typeof importIndex !== "number" || importIndex < 0 || importIndex >= [native_docs count]) {
            importIndex = 0;
        }
        doc = [native_docs objectAtIndex: importIndex];
        [doc showMessage:"Exporting to Principle..."];
        const doc_js = sketch.fromNative(doc);

        export_directory = exportPath;
        [[NSFileManager defaultManager] createDirectoryAtPath:export_directory withIntermediateDirectories:true attributes:null error:null];
        
        function get_artboard(l) {
            while (l && l.type != "Artboard") {
                l = l.parent
            }
            return l;
        }
        let artboards = [];
        if (shouldImportSelectedOnly == 1) {
            const selection = doc_js.selectedLayers.layers;
            let seen_artboards = [NSHashTable hashTableWithOptions:(NSHashTableObjectPointerPersonality | NSHashTableStrongMemory)];
            for (let selected of selection) {
                const artboard = get_artboard(selected);
                if (artboard && ![seen_artboards containsObject:artboard.sketchObject]) {
                    [seen_artboards addObject:seen_artboards.sketchObject];
                    artboards.push(artboard);
                }
            }
        }

        if (artboards.length == 0) {
            artboards = doc_js.selectedPage.layers; // [[doc currentPage] layers]; //
        }

        // filter before importing so we know how many artboards
        // will be imported, in order to update progress
        artboards = filter(artboards, (layer)=>{
            return (layer.type == "Artboard"); //isSymbolMaster(layer)
        });

        const principleNotificationName = "com.hoopersoftware.principle.import.progressupdate";
        
        let layers_metadata = [];
        for ( let i = 0; i < artboards.count(); i++) {
            const artboard_js = artboards[i];
            const artboard_native = artboard_js.sketchObject;
            
            const includeBGColor = artboard_js.background.includedInExport;
            artboard_js.background.includedInExport = false;

            try {
                [doc showMessage:"Exporting Artboard '"+artboard_js.name+"' to Principle..."];
                const progressInfo = {imported: i+1, total: [artboards count], artboardName: artboard_js.name};
                [[NSDistributedNotificationCenter defaultCenter] postNotificationName:principleNotificationName object:null userInfo:progressInfo deliverImmediately:true];

                layers_metadata.push(process_layer(artboard_js, "", false, false));
            } finally {
                artboard_js.background.includedInExport = includeBGColor;
            }
        }

        let json_data = [[NSMutableDictionary alloc] init];
        [json_data setValue:export_scale_factor forKey:@"scale"];
        [json_data setValue:layers_metadata forKey:@"layers"];

        let scaleJSON = [NSJSONSerialization dataWithJSONObject:json_data options:0 error:0];
        let settings_filepath = export_directory + "/data.json";
        [scaleJSON writeToFile: settings_filepath atomically:true];

        [doc showMessage:"Finished Exporting to Principle"];
    }
}());
