// This is based of off @babel/helper-builder-react-jsx package

import esutils from "esutils";
import * as t from "@babel/types";

/**
 * @typedef {Object} ElementState
 * @property {Object} tagExpr - tag node
 * @property {string} [tagName] - raw string tag name
 * @property {Array.<Object>} args - array of call arguments
 * @property {Object} [call] - optional call property that can be set to override the call expression returned
 */

/**
 * @param opts
 */
export default function(opts) {
    const visitor = {};
    
    visitor.JSXNamespacedName = function(path) {
        if (opts.throwIfNamespace) {
            throw path.buildCodeFrameError(
                `Namespace tags are not supported by default. React's JSX doesn't support namespace tags. \
You can turn on the 'throwIfNamespace' flag to bypass this warning.`,
            );
        }
    };
    
    visitor.JSXSpreadChild = function(path) {
        throw path.buildCodeFrameError(
            "Spread children are not supported in React.",
        );
    };
    
    visitor.JSXElement = {
        exit(path, file) {
            const callExpr = buildElementCall(path, file);
            if (callExpr) {
                path.replaceWith(t.inherits(callExpr, path.node));
            }
        },
    };
    
    visitor.JSXFragment = {
        exit(path, file) {
            if (opts.compat) {
                throw path.buildCodeFrameError(
                    "Fragment tags are only supported in React 16 and up.",
                );
            }
            const callExpr = buildFragmentCall(path, file);
            if (callExpr) {
                path.replaceWith(t.inherits(callExpr, path.node));
            }
        },
    };
    
    return visitor;
    
    function convertJSXIdentifier(node, parent) {
        if (t.isJSXIdentifier(node)) {
            if (node.name === "this" && t.isReferenced(node, parent)) {
                return t.thisExpression();
            } else if (esutils.keyword.isIdentifierNameES6(node.name)) {
                node.type = "Identifier";
            } else {
                return t.stringLiteral(node.name);
            }
        } else if (t.isJSXMemberExpression(node)) {
            return t.memberExpression(
                convertJSXIdentifier(node.object, node),
                convertJSXIdentifier(node.property, node),
            );
        } else if (t.isJSXNamespacedName(node)) {
            /**
             * If there is flag "throwIfNamespace"
             * print XMLNamespace like string literal
             */
            return t.stringLiteral(`${node.namespace.name}:${node.name.name}`);
        }
        
        return node;
    }
    
    function convertAttributeValue(node) {
        if (t.isJSXExpressionContainer(node)) {
            return node.expression;
        } else {
            return node;
        }
    }
    
    function convertAttribute(node) {
        const value = convertAttributeValue(node.value || t.booleanLiteral(true));
        
        if (t.isStringLiteral(value) && !t.isJSXExpressionContainer(node.value)) {
            value.value = value.value.replace(/\n\s+/g, " ");
            
            // "raw" JSXText should not be used from a StringLiteral because it needs to be escaped.
            if (value.extra && value.extra.raw) {
                delete value.extra.raw;
            }
        }
        
        if (t.isJSXNamespacedName(node.name)) {
            node.name = t.stringLiteral(
                node.name.namespace.name + ":" + node.name.name.name,
            );
        } else if (esutils.keyword.isIdentifierNameES6(node.name.name)) {
            node.name.type = "Identifier";
        } else {
            node.name = t.stringLiteral(node.name.name);
        }
        
        return t.inherits(t.objectProperty(node.name, value), node);
    }
    
    function buildElementCall(path, file) {
        if (opts.filter && !opts.filter(path.node, file)) return;
        
        const openingPath = path.get("openingElement");
        openingPath.parent.children = t.react.buildChildren(openingPath.parent);
        
        const tagExpr = convertJSXIdentifier(
            openingPath.node.name,
            openingPath.node,
        );
        const args = [];
        
        let tagName;
        if (t.isIdentifier(tagExpr)) {
            tagName = tagExpr.name;
        } else if (t.isLiteral(tagExpr)) {
            tagName = tagExpr.value;
        }
    
        /**
         * @type {ElementState}
         */
        const state = {
            tagExpr: tagExpr,
            tagName: tagName,
            args: args,
        };
        
        if (opts.pre) {
            opts.pre(state, file);
        }
        
        let attribs = openingPath.node.attributes;
        if (attribs.length) {
            attribs = buildOpeningElementAttributes(attribs, file);
        } else {
            attribs = t.nullLiteral();
        }
        
        args.push(attribs, ...path.node.children);
        
        if (opts.post) {
            opts.post(state, file);
        }
    
        // THIS IS THE LINE THAT USED TO BE HERE: (it maps JSX to a call of whatever the pragma-options is
        // return state.call || t.callExpression(state.callee, args);
        // THIS IS THE REPLACEMENT: (it maps JSX to a VNode-object)
        if(state.call) return state.call;
        else{
            let vnodeExpr = t.objectExpression([
                t.objectProperty(t.identifier('type'), args[0]),
                t.objectProperty(t.identifier('props'), args[1]),
                t.objectProperty(t.identifier('children'), t.arrayExpression(args.slice(2)))
            ]);
            if(state.callee) return t.callExpression(state.callee, [vnodeExpr]);
            else return vnodeExpr;
        }
    }
    
    function pushProps(_props, objs) {
        if (!_props.length) return _props;
        
        objs.push(t.objectExpression(_props));
        return [];
    }
    
    /**
     * The logic for this is quite terse. It's because we need to
     * support spread elements. We loop over all attributes,
     * breaking on spreads, we then push a new object containing
     * all prior attributes to an array for later processing.
     */
    
    function buildOpeningElementAttributes(attribs, file) {
        let _props = [];
        const objs = [];
        
        const useBuiltIns = file.opts.useBuiltIns || false;
        if (typeof useBuiltIns !== "boolean") {
            throw new Error(
                "transform-react-jsx currently only accepts a boolean option for " +
                "useBuiltIns (defaults to false)",
            );
        }
        
        while (attribs.length) {
            const prop = attribs.shift();
            if (t.isJSXSpreadAttribute(prop)) {
                _props = pushProps(_props, objs);
                objs.push(prop.argument);
            } else {
                _props.push(convertAttribute(prop));
            }
        }
        
        pushProps(_props, objs);
        
        if (objs.length === 1) {
            // only one object
            attribs = objs[0];
        } else {
            // looks like we have multiple objects
            if (!t.isObjectExpression(objs[0])) {
                objs.unshift(t.objectExpression([]));
            }
            
            const helper = useBuiltIns
                ? t.memberExpression(t.identifier("Object"), t.identifier("assign"))
                : file.addHelper("extends");
            
            // spread it
            attribs = t.callExpression(helper, objs);
        }
        
        return attribs;
    }
    
    function buildFragmentCall(path, file) {
        if (opts.filter && !opts.filter(path.node, file)) return;
        
        const openingPath = path.get("openingElement");
        openingPath.parent.children = t.react.buildChildren(openingPath.parent);
        
        const args = [];
        const tagName = null;
        const tagExpr = file.get("jsxFragIdentifier")();
    
        /**
         * @type {ElementState}
         */
        const state= {
            tagExpr: tagExpr,
            tagName: tagName,
            args: args,
        };
        
        if (opts.pre) {
            opts.pre(state, file);
        }
        
        // no attributes are allowed with <> syntax
        args.push(t.nullLiteral(), ...path.node.children);
        
        if (opts.post) {
            opts.post(state, file);
        }
        
        file.set("usedFragment", true);
        return state.call || t.callExpression(state.callee, args);
    }
}