"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const traverse_1 = require("@glas/traverse");
const ast_1 = require("../ast");
const ArrowFunctionExpression_1 = __importDefault(require("../ast/ArrowFunctionExpression"));
const AssignmentStatement_1 = __importDefault(require("../ast/AssignmentStatement"));
const common_1 = require("../common");
function convertExpressionWithNestedStatements(node) {
    const { location } = node;
    const containerName = "$";
    const containerRef = new ast_1.Reference({ location, name: containerName });
    const containerId = new ast_1.Declarator({ location, name: containerName });
    const isObjectExpression = ast_1.ObjectExpression.is(node);
    const isMap = isObjectExpression && node.isMap;
    const isArrayExpression = ast_1.ArrayExpression.is(node);
    const isSet = isArrayExpression && node.isSet;
    if (isObjectExpression && node.properties.find(ast_1.Statement.is)) {
        return new ast_1.CallExpression({
            location,
            callee: new ArrowFunctionExpression_1.default({
                params: [new ast_1.Parameter({ location, id: containerId })],
                expression: false,
                body: new ast_1.BlockStatement({
                    location,
                    body: traverse_1.traverse(node.properties, {
                        enter(e) {
                            //  don't traverse into call expressions
                            //  also don't recurse into expressions or expression statements
                            //  basically we only want to recurse into control flow statements
                            if (ast_1.CallExpression.is(e) || ast_1.Expression.is(e) || ast_1.ExpressionStatement.is(e)) {
                                return traverse_1.skip;
                            }
                        },
                        leave(e, ancestors, path) {
                            //  if this is an immediate child expression we treat it as a statement
                            //  so it gets converted to an ExpressionStatement down below.
                            let parent = ancestors[ancestors.length - 1];
                            if (ast_1.Property.is(e) && Array.isArray(parent)) {
                                if (isMap) {
                                    let { key, value } = e;
                                    if (ast_1.Identifier.is(key)) {
                                        //  keys are always computed for Maps.
                                        key = new ast_1.Reference(key);
                                    }
                                    return new ast_1.ExpressionStatement({
                                        expression: new ast_1.CallExpression({
                                            location,
                                            callee: new ast_1.MemberExpression({
                                                location,
                                                object: containerRef,
                                                property: new ast_1.Identifier({ location, name: "set" })
                                            }),
                                            arguments: [e.key, e.value]
                                        })
                                    });
                                }
                                else {
                                    return new AssignmentStatement_1.default({
                                        location,
                                        left: new ast_1.MemberExpression({
                                            location,
                                            object: containerRef,
                                            property: e.key,
                                        }),
                                        operator: "=",
                                        right: e.value,
                                    });
                                }
                            }
                        }
                    }).concat([new ast_1.ReturnStatement({ location, argument: containerRef })])
                })
            }),
            arguments: [
                isMap
                    ? new ast_1.CallExpression({ location, new: true, callee: new ast_1.Reference({ location, name: "Map" }), arguments: [] })
                    : new ast_1.ObjectExpression({ location, properties: [] })
            ]
        });
    }
    else if (isMap) {
        return new ast_1.CallExpression({
            location,
            new: true,
            callee: new ast_1.Reference({ location, name: "Map" }),
            arguments: [
                new ast_1.ArrayExpression({
                    location,
                    elements: node.properties.map(({ key, value }) => new ast_1.ArrayExpression({ location, elements: [key, value] }))
                })
            ]
        });
    }
    else if (isArrayExpression && node.elements.find(ast_1.Statement.is)) {
        //  TODO: extend ionscript.add to handle Sets and convert to use it.
        const push = new ast_1.Identifier({ location, name: isSet ? "add" : "push" });
        const mergePushElementsWithNext = new Array();
        return new ast_1.CallExpression({
            location,
            callee: new ArrowFunctionExpression_1.default({
                location,
                params: [new ast_1.Parameter({ location, id: containerId })],
                expression: false,
                body: new ast_1.BlockStatement({
                    location,
                    body: traverse_1.traverse(node.elements, {
                        enter(e) {
                            //  don't traverse into call expressions
                            //  also don't recurse into expressions or expression statements
                            //  basically we only want to recurse into control flow statements
                            if (ast_1.CallExpression.is(e) || ast_1.Expression.is(e) || ast_1.ExpressionStatement.is(e)) {
                                return traverse_1.skip;
                            }
                        },
                        leave(e, ancestors, path) {
                            //  if this is an immediate child expression we treat it as a statement
                            //  so it gets converted to an ExpressionStatement down below.
                            let parent = ancestors[ancestors.length - 1];
                            let statement = parent === node.elements;
                            if (ast_1.ExpressionStatement.is(e)) {
                                e = e.expression;
                                statement = true;
                            }
                            if ((ast_1.Expression.is(e) || ast_1.SpreadElement.is(e)) && Array.isArray(parent)) {
                                // see if the next peer element is an expression or expression statement
                                let mergeWithNext = false;
                                if (!isSet) {
                                    let nextPeer = ancestors[ancestors.length - 1][path[path.length - 1] + 1];
                                    if (ast_1.Expression.is(nextPeer) || ast_1.SpreadElement.is(nextPeer) || ast_1.ExpressionStatement.is(nextPeer))
                                        mergeWithNext = true;
                                }
                                if (mergeWithNext) {
                                    mergePushElementsWithNext.push(e);
                                    return traverse_1.remove;
                                }
                                let expression = new ast_1.CallExpression({
                                    location,
                                    callee: new ast_1.MemberExpression({
                                        location,
                                        object: containerRef,
                                        property: push
                                    }),
                                    arguments: [...mergePushElementsWithNext, e]
                                });
                                mergePushElementsWithNext.length = 0;
                                return statement ? new ast_1.ExpressionStatement({ location, expression }) : expression;
                            }
                        }
                    }).concat([new ast_1.ReturnStatement({ location, argument: containerRef })])
                })
            }),
            arguments: [
                isSet
                    ? new ast_1.CallExpression({ location, new: true, callee: new ast_1.Reference({ location, name: "Set" }), arguments: [] })
                    : new ast_1.ArrayExpression({ location, elements: [] })
            ]
        });
    }
    else if (isSet) {
        return new ast_1.CallExpression({
            location,
            new: true,
            callee: new ast_1.Reference({ location, name: "Set" }),
            arguments: [node.patch({ isSet: false })]
        });
    }
    else if (ast_1.ElementExpression.is(node)) {
        let hasNonPropertyStatements = node.children.find(a => ast_1.Statement.is(a)) != null;
        const propertiesName = "$";
        const childrenName = "children";
        let kind = node.kind;
        //  lower case references use strings as they represent elements
        if (ast_1.Reference.is(kind) && kind.name[0] === kind.name[0].toLowerCase()) {
            kind = new ast_1.Literal({ location, value: kind.name });
        }
        if (hasNonPropertyStatements) {
            const mergePushElementsWithNext = new Array();
            let addedChildren = false;
            return new ast_1.CallExpression({
                location,
                callee: new ArrowFunctionExpression_1.default({
                    location,
                    params: [
                        new ast_1.Parameter({ location, id: new ast_1.Declarator({ location, name: propertiesName }) }),
                    ],
                    expression: false,
                    body: new ast_1.BlockStatement({
                        location,
                        body: traverse_1.traverse(node.children, {
                            enter(e) {
                                //  don't traverse into call expressions
                                //  also don't recurse into expressions or expression statements
                                //  basically we only want to recurse into control flow statements
                                if (ast_1.CallExpression.is(e) || ast_1.Expression.is(e) || ast_1.ExpressionStatement.is(e)) {
                                    return traverse_1.skip;
                                }
                            },
                            leave(e, ancestors, path) {
                                //  if this is an immediate child expression we treat it as a statement
                                //  so it gets converted to an ExpressionStatement down below.
                                let parent = ancestors[ancestors.length - 1];
                                if (ast_1.ExpressionStatement.is(e)) {
                                    e = e.expression;
                                }
                                if (ast_1.Property.is(e) && Array.isArray(parent)) {
                                    //  we do NOT allow properties to be set AFTER children have been added
                                    //  it would make it difficult to differentiate the properties from the children.
                                    if (addedChildren) {
                                        throw common_1.SemanticError(`Properties must be set before children`, e);
                                    }
                                    return new AssignmentStatement_1.default({
                                        location,
                                        left: new ast_1.MemberExpression({
                                            location,
                                            object: new ast_1.Reference({ location, name: propertiesName }),
                                            property: e.key,
                                        }),
                                        operator: "=",
                                        right: e.value,
                                    });
                                }
                                if ((ast_1.Expression.is(e) || ast_1.SpreadElement.is(e)) && Array.isArray(parent)) {
                                    addedChildren = true;
                                    // see if the next peer element is an expression or expression statement
                                    let mergeWithNext = false;
                                    if (!isSet) {
                                        let nextPeer = ancestors[ancestors.length - 1][path[path.length - 1] + 1];
                                        if (ast_1.Expression.is(nextPeer) || ast_1.SpreadElement.is(nextPeer) || ast_1.ExpressionStatement.is(nextPeer))
                                            mergeWithNext = true;
                                    }
                                    if (mergeWithNext) {
                                        mergePushElementsWithNext.push(e);
                                        return traverse_1.remove;
                                    }
                                    let expression = new ast_1.CallExpression({
                                        location,
                                        callee: new ast_1.MemberExpression({
                                            location,
                                            object: new ast_1.MemberExpression({
                                                location,
                                                object: new ast_1.Reference({ location, name: propertiesName }),
                                                property: new ast_1.Identifier({ location, name: childrenName }),
                                            }),
                                            property: new ast_1.Identifier({ location, name: "push" }),
                                        }),
                                        arguments: [...mergePushElementsWithNext, e]
                                    });
                                    mergePushElementsWithNext.length = 0;
                                    return new ast_1.ExpressionStatement({ location, expression });
                                }
                            }
                        }).concat([
                            new ast_1.ReturnStatement({
                                location,
                                argument: new ast_1.CallExpression({
                                    location,
                                    callee: new ast_1.Reference({ location, name: "jsx" }),
                                    arguments: [
                                        kind,
                                        new ast_1.Reference({ location, name: propertiesName }),
                                    ]
                                })
                            })
                        ])
                    })
                }),
                arguments: [
                    new ast_1.ObjectExpression({
                        location,
                        properties: [
                            ...node.properties,
                            new ast_1.Property({
                                location,
                                key: new ast_1.Identifier({ location, name: childrenName }),
                                value: new ast_1.ArrayExpression({ location, elements: [] })
                            })
                        ]
                    }),
                ]
            });
        }
        // we are converting this into a simple createElement(kind, properties, ...children) function call
        let properties = [...node.properties];
        let children = [];
        for (let child of node.children) {
            if (ast_1.Property.is(child)) {
                properties.push(child);
            }
            else {
                if (!(ast_1.Expression.is(child) || ast_1.SpreadElement.is(child))) {
                    throw common_1.SemanticError("Expected Expression or SpreadElement", child);
                }
                children.push(child);
            }
        }
        //  TODO: We should probably just output JSX
        let args = [
            kind,
            new ast_1.ObjectExpression({
                location,
                properties: [
                    ...properties,
                    new ast_1.Property({
                        location,
                        key: new ast_1.Identifier({ location, name: "children" }),
                        value: new ast_1.ArrayExpression({ location, elements: children })
                    })
                ]
            })
            // ...children
        ];
        return new ast_1.CallExpression({
            location,
            callee: new ast_1.Reference({ location, name: "jsx" }),
            arguments: args
        });
    }
}
function controlFlowToExpressions(root, options) {
    return traverse_1.traverse(root, {
        enter(node) {
            if (ast_1.Program.is(node)) {
                return traverse_1.skip;
            }
        },
        leave(node) {
            let { location } = node;
            if (ast_1.Program.is(node)) {
                let hasJsxElements = false;
                let program = traverse_1.traverse(node, {
                    leave(node) {
                        var _a;
                        if (ast_1.ElementExpression.is(node)) {
                            hasJsxElements = true;
                        }
                        if (ast_1.CallExpression.is(node) && node.arguments.find(ast_1.Statement.is)) {
                            return node.patch({
                                arguments: [
                                    new ast_1.SpreadElement({
                                        location,
                                        argument: convertExpressionWithNestedStatements(new ast_1.ArrayExpression({ location, elements: node.arguments }))
                                    })
                                ]
                            });
                        }
                        if (ast_1.OutlineOperation.is(node)) {
                            let { operator } = node;
                            let operands = new ast_1.ArrayExpression({ location, elements: node.operands });
                            return new ast_1.CallExpression({
                                location,
                                callee: new ast_1.MemberExpression({
                                    location,
                                    object: (_a = convertExpressionWithNestedStatements(operands)) !== null && _a !== void 0 ? _a : operands,
                                    property: new ast_1.Identifier({ location, name: "reduce" })
                                }),
                                arguments: [
                                    new ArrowFunctionExpression_1.default({
                                        location,
                                        params: [
                                            new ast_1.Parameter({ location, id: new ast_1.Declarator({ location, name: "a" }) }),
                                            new ast_1.Parameter({ location, id: new ast_1.Declarator({ location, name: "c" }) }),
                                        ],
                                        body: new ast_1.BinaryExpression({
                                            location,
                                            left: new ast_1.Reference({ location, name: "a" }),
                                            operator,
                                            right: new ast_1.Reference({ location, name: "c" })
                                        }),
                                        expression: true
                                    })
                                ]
                            });
                        }
                        return convertExpressionWithNestedStatements(node);
                    }
                });
                // add jsx import statement
                if (hasJsxElements && !common_1.hasDeclarator(program.body, "jsx")) {
                    program = program.patch({
                        body: [
                            new ast_1.ImportDeclaration({
                                location,
                                specifiers: [
                                    new ast_1.ImportSpecifier({
                                        location,
                                        local: new ast_1.Declarator({ name: "jsx", location }),
                                        imported: new ast_1.Reference({ name: "jsx", location })
                                    })
                                ],
                                source: new ast_1.Literal({ value: "react/jsx-runtime", location })
                            }),
                            ...program.body
                        ]
                    });
                }
                return program;
            }
        }
    });
}
exports.default = controlFlowToExpressions;
