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

function print(m) {
    log("principle: " + m);
};

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() {

    function assert(condition, message) {
        if (!condition) {
            throw new Error(message || "Assertion failed");
        }
    }

    const sketch = require('sketch');

    const RasterizeStyle = Object.freeze({
        None: 0,
        AllExceptShadowAndBorder: 1,  // bit pattern 001
        AllExceptShadow: 3,  // bit pattern 011
        All: 7 // bit pattern 111
    });

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

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

    const RasterizeBorderMask = 2;
    const RasterizeShadowMask = 4;

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

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

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

        let remove_after_processed = false;
        const originalLayer = layer;

        if (isSymbolInstance(layer)) {
            layer = detachSymbolRecursively(layer);

            if (!layer) {
                return;
            }

            remove_after_processed = true;
            [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 < influenceRect(layer).size.width ) {
                [layer adjustFrameToFit];
            }
        }

        let imagePath, 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] != BorderPosition.INSIDE) {
                        //                    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 = influenceRect(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];

            let sublayer_info = [];

            for (let i = 0; i < [sublayers count]; i++) {
                sublayer_info.pushNonNull(process_layer([sublayers objectAtIndex:i], 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]] || [layer isMemberOfClass:[MSArtboardGroup 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, is_flipped_horizontal, is_flipped_vertical) {
        const path_to_file = export_directory + "/assets/" + assetNumber + ".png";
        assetNumber++;

        const style = [layer style];
        const blur_nullable = [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 {
            let 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 exportRequest = MSExportRequest.new();
        exportRequest.rect = influenceRect(layer_to_render);
        exportRequest.scale = export_scale_factor;
        exportRequest.shouldTrim = false;
        exportRequest.includeArtboardBackground = ![[layer class] isMemberOfClass:[MSArtboardGroup class]];

//        if (exportRequest.configureForLayerAncestry_layerOptions_includedIDs_) {
//            [exportRequest configureForLayerAncestry:layer_to_render.ancestry() layerOptions:nil includedIDs:nil];
//        } else {
            [exportRequest configureForLayerAncestry:layer_to_render.ancestry() layerOptions:nil];
//        }

        [doc saveExportRequest:exportRequest toFile:path_to_file];

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

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

    function influenceRect(layer, enable_logging) {
        enable_logging = false;
        const style = [layer style];

        let frame = CGRectFromMSRect(layer.frame());

        const log_func = enable_logging ? log : function(){};

        log_func(`initial frame ${CGRectToString(frame)}`);

        // expand for border
        let border_outset = 0;
        const borders = [style borders];
        for (let i = 0; i < borders.count(); i++) {
            const border = borders[i];
            if (![border isEnabled]) continue;
            switch ([border position]) {
                case BorderPosition.OUTSIDE:
                    border_outset = Math.max(border_outset, [border thickness]);
                    break;
                case BorderPosition.CENTER:
                    border_outset = Math.max(border_outset, [border thickness]/2);
                    break;
            }
        }

        log_func(`border outset ${border_outset}`);

        // expand for shadow
        let left = 0;
        let right = 0;
        let up = 0;
        let down = 0;
        const shadows = [style shadows];
        for (let i = 0; i < shadows.count(); i++) {
            const shadow = shadows[i];
            if (![shadow isEnabled]) continue;
            
            const expansion = [shadow spread] + [shadow blurRadius];
            left    = Math.min(left,    shadow.offsetX()-expansion);
            right   = Math.max(right,   shadow.offsetX()+expansion);
            up      = Math.min(up,      shadow.offsetY()-expansion);
            down    = Math.max(down,    shadow.offsetY()+expansion);
        }

        log_func(`shadow expansion left ${left} right ${right} down ${down} up ${up}`);

        //
        // 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
        const blur = [style blur];
        if (blur && [blur isEnabled] && [blur type] != BlurType.BACKGROUND) {
            const blur_expansion = [blur radius]*2;
            log_func(`blur_expansion ${blur_expansion}`);
            frame = CGRectInset(frame,-blur_expansion,-blur_expansion);
        }

        return CGRectIntegral(frame);//CGRectInset(,-1,-1); // expand by 1 pixel for antialiasing
    }

    function detachSymbolRecursively(layer) {

        // we used to detach symbols ourselves in order to maintain stable uuids across imports, but
        // mwith sketch's symbols getting more complicated, it became error prone to
        // detach ourselves, so now we just call sketch's detach function

        // layer = _internal_detachSymbolRecursively(layer, [sketch.fromNative(layer).overrides], null);
        const duplicate_layer = sketch.fromNative(layer).duplicate();
        layer = duplicate_layer.detach({ recursively: true})?.sketchObject;

        duplicate_layer.remove();
        return layer;

        function copyIDsFromLayerTree(source_layer, destination_layer) {
            destination_layer.objectID = [source_layer objectID];

            if (isSymbolInstance(destination_layer)) {
                return;
            }

            const destination_sublayers = get_sublayers(destination_layer);
            const source_sublayers = get_sublayers(source_layer);
            const 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]);
            }
        }

        function getOverrides(uuid, override_stack) {
            let matching_overrides = {};
            let currentPath = "";
            for (let level=0; level < override_stack.length; level++) {
                const overrides = override_stack[level];
                for (let o of overrides) {
                    if (o.path !== currentPath) continue;
                    if (!Object.has_own_property(matching_overrides,p.property) || !o.isDefault) {
                        matching_overrides[o.property] = o.value;
                    }
                }
                currentPath = currentPath.substring(currentPath.indexOf('/') + 1);
            }

            return matching_overrides;
        }

        function _internal_detachSymbolRecursively(layer, override_stack, uuidpath) {
            const is_topmost_call = !uuidpath;
            const localPath = (uuidpath ? (uuidpath+"/") : "")+[layer objectID];
            const api_layer = sketch.fromNative(layer);

            if (isSymbolInstance(layer)) {
                let master = [layer symbolMaster];
                const overridePath = localPath.substring(localPath.indexOf('/') + 1);

                //            print(` ## detaching (${ [layer name] }) ${api_layer.id} from (${ master?.name() })`); //   ${api_master.symbolId}
                //            print(`overridePath ${overridePath} localPath ${localPath}`);
                //
                //            {
                //                print("overrides:");
                //                for (var p of api_layer.overrides) {
                //                    if (p.property !== "symbolID") continue;
                //                    print(`symbol override path: ${p.path} value ${p.value}  isDefault: ${p.isDefault}`); //
                //                }
                //                print("");
                //            }

                const override = overrides.find(e => e.path === overridePath && e.property === "symbolID");

                if (override) {
                    //                print(`override value ${override.value}`);
                    if (override.value !== "") {
                        master = [[doc documentData] symbolWithID: override.value];

                        //                    print(`overridden master ${ sketch.fromNative(master).symbolId } (${master?.name()}) vs original ${sketch.fromNative([layer symbolMaster]).symbolId} (${[layer symbolMaster]?.name()})`);
                    } else {
                        master = null;
                    }
                }

                if (master == null) { return null; }

                [idToMasterMap setObject:master forKey:[layer objectID]];

                let sublayers = [master layers];

                if ([sublayers count] === 0) return null;

                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
                for (let k = 0; k < [sublayers count]; k++) {
                    const originalSublayer = sublayers[k];
                    const newSublayer = [originalSublayer copy];

                    [newGroup addLayer:newSublayer];
                    copyIDsFromLayerTree(originalSublayer, newSublayer);
                }

                //resize layers
                let old_size = CGSizeMake([[master frame] width], [[master frame] height]);
                [newGroup resizeChildrenWithOldSize:old_size];

                if (uuidpath !== null) [layer removeFromParent];
                uuidpath = localPath;
                layer = newGroup;
            } else {
                let document = sketch.fromNative(doc);
                // TODO: find overrides for layer
                const overrides = override_stack[0];
                for (let i = 0; i < overrides.length; i++) {
                    const override = overrides[i];
                    if (override.path != localPath) { continue; }

                    if (override.property === "stringValue" && isTextLayer(layer)) {
                        api_layer.text = override.value;
                    } else if (override.property === "image") {
                        if ([layer isMemberOfClass: [MSBitmapLayer class]]) {
                            api_layer.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 {
                        print("unhandled property: "+override.property);
                    }
                }
            }

            //detach sublayers, remove layers that cannot be detached
            const sublayers = get_sublayers(layer);
            for (let s = 0; s < [sublayers count]; s++) {
                const sublayer_el = sublayers[s];
                if (!_internal_detachSymbolRecursively(sublayer_el, override_stack, uuidpath)) {
                    [sublayer_el removeFromParent];
                }
            }

            return layer;
        }

    }

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

        export_scale_factor = assetScale;

        const docs = [[NSApplication sharedApplication] orderedDocuments];
        if (typeof importIndex !== "number" || importIndex < 0 || importIndex >= [docs count]) {
            importIndex = 0;
        }
        doc = [docs objectAtIndex: importIndex];

        [doc showMessage:"Exporting to Principle..."];

        export_directory = exportPath;
        [[NSFileManager defaultManager] createDirectoryAtPath:export_directory withIntermediateDirectories:true attributes:null error:null];

        let artboards;
        const current_page = [doc currentPage];
        const selectedLayers = [current_page selectedLayers];
        const selection = [selectedLayers isKindOfClass: [NSArray class]] ? selectedLayers : [selectedLayers layers];

        if (shouldImportSelectedOnly == 1) {
            let artboard_set = [NSHashTable hashTableWithOptions:(NSHashTableObjectPointerPersonality | NSHashTableStrongMemory)];
            for (let i = 0; i < [selection count]; i++) {
                let selected = [selection objectAtIndex:i];
                if (selected.parentArtboard) {
                    [artboard_set addObject: [selected parentArtboard]];
                }
            }
            artboards = [artboard_set allObjects];
        }

        if (!artboards || [artboards count] == 0) {
            artboards = [current_page layers];
        }

        // filter before importing so we know how many artboards
        // will be imported, in order to update progress
        artboards = filter(artboards, (layer)=>([layer isMemberOfClass:[MSArtboardGroup class]] || isSymbolMaster(layer)));

        let layers_metadata = [];
        for ( let i = 0; i < artboards.count(); i++) {
            const artboard = artboards[i];
            if (![artboard isMemberOfClass:[MSArtboardGroup class]] && !isSymbolMaster(artboard)) continue;

            // we used to use this for render_in_place layers
//            const artboard_frame = [artboard frame];
//            const background_layer = new sketch.Shape({
//            parent: sketch.fromNative(artboard),
//            frame: new sketch.Rectangle(0,0,[artboard_frame width],[artboard_frame height])
//            });
//            background_layer.style.fills = [{
//            color: '#ffffffff',
//            fill: sketch.Style.FillType.Color,
//            }];
//            background_layer.moveToBack();
//            if ([artboard hasBackgroundColor]) {
//                const bg_style = background_layer.style.sketchObject;
//                const fill = [bg_style fills][0];
//                fill.color = [artboard backgroundColor];
//            }

            const includeBGColor = [artboard includeBackgroundColorInExport];
            [artboard setIncludeBackgroundColorInExport:false];

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

                layers_metadata.push(process_layer(artboard, "", false, false));
            } finally {
                [artboard setIncludeBackgroundColorInExport:includeBGColor];
//                background_layer.remove();
            }
        }

        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"];
    };
}());
